Forráskód Böngészése

Merge https://github.com/gcushen/hugo-academic

Xℹ Ruoyao 4 éve
szülő
commit
22ce3810a5
90 módosított fájl, 2250 hozzáadás és 1497 törlés
  1. 10 7
      .github/ISSUE_TEMPLATE/bug-report.md
  2. 1 2
      .github/ISSUE_TEMPLATE/feature-request.md
  3. 52 18
      .github/contributing.md
  4. 4 5
      .github/stale.yml
  5. 6 4
      .github/support.md
  6. 1 1
      LICENSE.md
  7. 4 0
      netlify-cms-academic/config.yaml
  8. 180 12
      netlify-cms-academic/static/admin/config.yml
  9. 1 1
      scripts/list_language_packs.py
  10. 11 0
      themes/hello-world/assets/scss/template.scss
  11. 16 0
      themes/hello-world/config.yaml
  12. 3 0
      themes/hello-world/go.mod
  13. 4 4
      wowchemy/archetypes/event/index.md
  14. BIN
      wowchemy/assets/images/icon.png
  15. 0 47
      wowchemy/assets/js/load-theme.js
  16. 24 0
      wowchemy/assets/js/wowchemy-animation.js
  17. 18 0
      wowchemy/assets/js/wowchemy-init.js
  18. 3 1
      wowchemy/assets/js/wowchemy-search.js
  19. 255 0
      wowchemy/assets/js/wowchemy-theming.js
  20. 28 0
      wowchemy/assets/js/wowchemy-utils.js
  21. 551 716
      wowchemy/assets/js/wowchemy.js
  22. 1 0
      wowchemy/assets/scss/main.scss
  23. 15 0
      wowchemy/assets/scss/wowchemy/_breadcrumb.scss
  24. 66 0
      wowchemy/assets/scss/wowchemy/_callouts.scss
  25. 15 8
      wowchemy/assets/scss/wowchemy/_content.scss
  26. 1 1
      wowchemy/assets/scss/wowchemy/_dark.scss
  27. 2 6
      wowchemy/assets/scss/wowchemy/_footer.scss
  28. 11 1
      wowchemy/assets/scss/wowchemy/_integrations.scss
  29. 70 77
      wowchemy/assets/scss/wowchemy/_root.scss
  30. 24 1
      wowchemy/assets/scss/wowchemy/_search.scss
  31. 34 0
      wowchemy/assets/scss/wowchemy/_shortcodes.scss
  32. 40 37
      wowchemy/assets/scss/wowchemy/_widgets.scss
  33. 3 0
      wowchemy/assets/scss/wowchemy/wowchemy.scss
  34. 17 1
      wowchemy/config.yaml
  35. 11 0
      wowchemy/data/fonts/cyberpunk.toml
  36. 26 0
      wowchemy/data/themes/cyberpunk.toml
  37. 25 0
      wowchemy/data/themes/earth.toml
  38. 2 2
      wowchemy/data/themes/ocean.toml
  39. 1 1
      wowchemy/data/wowchemy.toml
  40. 2 0
      wowchemy/i18n/da.yaml
  41. 8 0
      wowchemy/i18n/de.yaml
  42. 12 3
      wowchemy/i18n/en.yaml
  43. 5 1
      wowchemy/i18n/es.yaml
  44. 13 10
      wowchemy/i18n/he.yaml
  45. 34 23
      wowchemy/layouts/_default/baseof.html
  46. 0 0
      wowchemy/layouts/event/single.html
  47. 8 0
      wowchemy/layouts/partials/book_layout.html
  48. 2 2
      wowchemy/layouts/partials/book_menu.html
  49. 42 15
      wowchemy/layouts/partials/book_sidebar.html
  50. 10 0
      wowchemy/layouts/partials/breadcrumb.html
  51. 12 0
      wowchemy/layouts/partials/breadcrumb_helper.html
  52. 37 0
      wowchemy/layouts/partials/functions/get_social_link.html
  53. 2 0
      wowchemy/layouts/partials/functions/parse_theme.html
  54. 2 2
      wowchemy/layouts/partials/jsonld/main.html
  55. 6 4
      wowchemy/layouts/partials/li_card.html
  56. 6 4
      wowchemy/layouts/partials/li_compact.html
  57. 9 0
      wowchemy/layouts/partials/marketing/microsoft_clarity.html
  58. 14 1
      wowchemy/layouts/partials/navbar.html
  59. 3 3
      wowchemy/layouts/partials/page_author_card.html
  60. 1 1
      wowchemy/layouts/partials/page_header.html
  61. 1 1
      wowchemy/layouts/partials/page_metadata.html
  62. 2 2
      wowchemy/layouts/partials/page_metadata_authors.html
  63. 4 2
      wowchemy/layouts/partials/portfolio_li_card.html
  64. 3 1
      wowchemy/layouts/partials/portfolio_li_showcase.html
  65. 16 0
      wowchemy/layouts/partials/search.html
  66. 1 11
      wowchemy/layouts/partials/site_footer.html
  67. 6 6
      wowchemy/layouts/partials/site_footer_license.html
  68. 14 7
      wowchemy/layouts/partials/site_head.html
  69. 21 11
      wowchemy/layouts/partials/site_js.html
  70. 40 11
      wowchemy/layouts/partials/widget_page.html
  71. 4 2
      wowchemy/layouts/partials/widgets/about.html
  72. 31 36
      wowchemy/layouts/partials/widgets/accomplishments.html
  73. 9 17
      wowchemy/layouts/partials/widgets/blank.html
  74. 113 118
      wowchemy/layouts/partials/widgets/contact.html
  75. 42 47
      wowchemy/layouts/partials/widgets/experience.html
  76. 36 26
      wowchemy/layouts/partials/widgets/featured.html
  77. 2 2
      wowchemy/layouts/partials/widgets/hero.html
  78. 36 51
      wowchemy/layouts/partials/widgets/pages.html
  79. 4 1
      wowchemy/layouts/partials/widgets/people.html
  80. 45 67
      wowchemy/layouts/partials/widgets/portfolio.html
  81. 24 30
      wowchemy/layouts/partials/widgets/tag_cloud.html
  82. 1 1
      wowchemy/layouts/project/single.html
  83. 15 6
      wowchemy/layouts/publication/single.html
  84. 1 1
      wowchemy/layouts/shortcodes/chart.html
  85. 1 1
      wowchemy/layouts/shortcodes/figure.html
  86. 4 4
      wowchemy/layouts/shortcodes/gallery.html
  87. 5 12
      wowchemy/layouts/shortcodes/spoiler.html
  88. 3 0
      wowchemy/test/config.yaml
  89. 1 0
      wowchemy/test/content/home/index.md
  90. 1 1
      wowchemy/test/view.sh

+ 10 - 7
.github/ISSUE_TEMPLATE/bug-report.md

@@ -4,16 +4,17 @@ about: Create a report to help us improve
 title: ''
 labels: ''
 assignees: ''
-
 ---
 
-Welcome to Academic's GitHub repo 👋
+Welcome to Wowchemy's GitHub repo 👋
 
 We use GitHub only for bug reports and feature requests - it's our project management tool.
 
-For **help** and **questions**, please join our **[community chat](https://spectrum.chat/academic)** or use the **[forum](https://discourse.gohugo.io/c/themes)** 🚑.
+🚑 For **help** and **questions**, solve common issues with the [Troubleshooting Guide](https://wowchemy.com/docs/faq/).
+
+For other issues, search if your question has already been asked on the community **[chat](https://discord.gg/z8wNYzb)**  and **[forum](https://github.com/wowchemy/wowchemy-hugo-modules/discussions)**, and if not, consider posting a new thread there.
 
-Also, you can search and browse the extensive [Academic](https://sourcethemes.com/academic/docs/) and [Hugo](https://gohugo.io/documentation/) **documentation**.
+Also, you can search and browse the extensive [Wowchemy](https://wowchemy.com/docs/) and [Hugo](https://gohugo.io/documentation/) **documentation**.
 
 For questions on _Blogdown_, please reach out to the [Blogdown community](https://github.com/rstudio/blogdown).
 
@@ -34,8 +35,10 @@ A clear and concise description of what you expected to happen.
 
 ### Technical details:
 
-* Academic Version:
-* Hugo Version:
-* Browser/OS:
+* Link to your GitHub project: 
+* Wowchemy Version (from your `go.mod`): 
+* Hugo Version (run `hugo version`): 
+* Browser/OS: 
+* Wowchemy Template: 
 
 If applicable, add screenshots to help explain the issue.

+ 1 - 2
.github/ISSUE_TEMPLATE/feature-request.md

@@ -2,9 +2,8 @@
 name: "\U0001F680 Feature request"
 about: Suggest an idea for this project
 title: ''
-labels: ''
+labels: 'Proposal'
 assignees: ''
-
 ---
 
 ## Feature Request

+ 52 - 18
.github/contributing.md

@@ -1,43 +1,77 @@
 # Contributing to Wowchemy
 
-For **help**, **support**, and **questions** please use our **[community chat](https://discord.gg/z8wNYzb)**  🚑.
+Thanks for being interested in contributing! We’re so glad you want to help!
 
----
+We want contributing to Wowchemy to be fun, enjoyable, and educational for anyone and everyone. All contributions are welcome, including new plugins (such as new widgets, shortcodes, theme packs, and language packs), templates, features, documentation as well as updates and tweaks, blog posts, YouTube tutorials, live streaming customizations, meetups, and more.
 
 ## Where to Start
 
+Join the **Contributing** channel on the **[community Discord](https://discord.gg/z8wNYzb)**.
+
+### For technical contributions
+
 Learn [how to contribute code on Github](https://codeburst.io/a-step-by-step-guide-to-making-your-first-github-contribution-5302260a2940).
 
-If you're a developer looking to contribute, but you're not sure where to begin, check out the [good first issue](https://github.com/gcushen/hugo-academic/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) label on Github, which contains small tasks that have been specifically flagged as being friendly to new contributors.
+If you're a developer looking to contribute, but you're not sure where to begin, check out the [good first issue](https://github.com/wowchemy/wowchemy-hugo-modules/issues?q=is%3Aopen+is%3Aissue+label%3A%22good+first+issue%22) label on Github, which contains small tasks that have been specifically flagged as being friendly to new contributors.
+
+After that, if you're looking for something a little more challenging to sink your teeth into, there's a broader [help wanted](https://github.com/wowchemy/wowchemy-hugo-modules/labels/help%20wanted) label encompassing issues which need some love.
+
+If you have a straightforward bug fix or improvement, feel free to contribute it in a [Pull Request](https://github.com/wowchemy/wowchemy-hugo-modules/pulls) for the community to review.
+
+If you have an idea for a new feature, please start by [searching the issues](https://github.com/wowchemy/wowchemy-hugo-modules/issues) to check that the feature has not already been suggested and then suggest it by [opening a new issue](https://github.com/wowchemy/wowchemy-hugo-modules/issues/new/choose), as adding new features to Wowchemy first requires some analysis around the design and spec.
+
+### Contribute a widget
+
+[Create and publish your own widget](https://github.com/wowchemy/wowchemy-widget-starter)
+
+### Contribute a shortcode
 
-After that, if you're looking for something a little more challenging to sink your teeth into, there's a broader [help wanted](https://github.com/gcushen/hugo-academic/labels/help%20wanted) label encompassing issues which need some love.
+[Create and publish your own shortcode](https://github.com/wowchemy/wowchemy-shortcode-starter)
 
-If you have a straightforward bug fix or improvement, feel free to contribute it in a [Pull Request](https://github.com/gcushen/hugo-academic/pulls).
+### Contribute a language pack 
 
-If you have an idea for a new feature, please start by [searching the issues](https://github.com/gcushen/hugo-academic/issues) to check that the feature has not already been suggested and then suggest it by [opening a new issue](https://github.com/gcushen/hugo-academic/issues/new/choose), as adding new features to Academic first requires some analysis around the design and spec.
+To contribute a **new language pack** or an improvement to a language pack, refer to the [language pack guide](https://wowchemy.com/docs/language/#create-or-modify-a-language-pack). Once created, [fork Wowchemy Hugo Modules](https://github.com/wowchemy/wowchemy-hugo-modules), place your language pack in `wowchemy/i18n/`, add the name of the language to `wowchemy/data/i18n/language.yaml`, and open a Pull Request on Github with these two files.
 
-To contribute a **new language pack**, refer to the [language pack guide](https://sourcethemes.com/academic/docs/language/#create-or-modify-a-language-pack). Once created, place you language pack in `academic/i18n/`, add the name of the language to `academic/data/i18n/language.yaml`, and open a Pull Request on Github with these two files.
+### Contribute a theme pack
 
-To contribute to **Academic Admin**, the automatic publication importer, refer to [its dedicated Github repository](https://github.com/sourcethemes/academic-admin).
+[View the guide](https://wowchemy.com/docs/customization/#share-your-theme) to contributing a color and font theme pack.
 
-## Stickers
+### Contribute a template
 
-🖼️ [Decorate your laptop or journal with an Academic **sticker**](https://www.redbubble.com/people/neutreno/works/34387919-academic)
+Consider forking a bare-bones template such as [Hello Starter](https://github.com/wowchemy/starter-hello-world) on GitHub and building up your own template using the Wowchemy Hugo Module. Reach out on the **Contributing** channel in Discord to submit your template.
 
-## Donations
+### Contribute to the Publication importer
 
-As a pure community-driven open source project, we welcome your support:
+To contribute to **Hugo Academic CLI**, the automatic publication importer, refer to [its dedicated Github repository](https://github.com/wowchemy/hugo-academic-cli) and Issue queue.
+
+## Become a backer
+
+To help us develop this free software sustainably under the open source license, we ask all individuals and businesses that use it to help support its ongoing maintenance and development via sponsorship:
 
   - ☕️ [**Donate a coffee**](https://paypal.me/cushen)
-  - 💵 [Become a backer on **Patreon**](https://www.patreon.com/cushen)
+  - ❤️ [**Become a sponsor and unlock awesome rewards**](https://wowchemy.com/plans/)
 
 ## Other ways to help
 
 If you're not a developer there are still plenty of ways that you can help. We always need help with:
 
-- Helping our Academic community on the [chat](https://spectrum.chat/academic)
-- Improving the [documentation](https://sourcethemes.com/academic/docs/) and writing tutorials
-  - Just click the _Edit_ button at the bottom of pages or contribute to the [documentation repository](https://github.com/sourcethemes/academic-www)
+- Helping the Wowchemy community on the [chat](https://discord.gg/z8wNYzb) and [forum](https://github.com/wowchemy/wowchemy-hugo-modules/discussions)
+- Investigating and reviewing open [Issues](https://github.com/wowchemy/wowchemy-hugo-modules/issues) and [Pull Requests](https://github.com/wowchemy/wowchemy-hugo-modules/pulls)
+  - Give a thumbs up 👍 to upvote a feature request you would like to use
+- Improving the [documentation](https://wowchemy.com/docs/) and writing tutorials
+  - Just click the _Edit_ button at the bottom of pages or open an issue with your proposed improvement
 - Testing and quality assurance
-- Hosting local Academic themed events or meetups
-- Promoting Academic to others by blogging, vlogging, code streaming, talking etc.
+- Hosting local Wowchemy themed events or meetups
+- Promoting Wowchemy to others by blogging, vlogging, code streaming, talking etc.
+
+## Scope
+
+Please be _mindful_ that although we encourage feature requests, we cannot expand the scope of the project in every possible direction. There will be feature requests that don't make the roadmap.
+
+Every feature requires effort not just to analyse the requirements, design it, implement it, test it, document it, merge it, write release notes for it, and release it, but also to continuously support users with it and maintain it (fixing and refactoring the feature as the project and its dependencies evolve).
+
+The more regular active volunteers (rather than one-off contributors) we have supporting users and maintaining the project, the more feasible it becomes to expand the scope of the project.
+
+The project's scope also has to be constrained so that it doesn't get too complex and unwieldy, from an architectural perspective, a testing perspective, and from a usability perspective.
+
+Plugins (widgets, shortcodes, theme packs, language packs, and third-party JavaScript integrations) as well as templates allow the community to add major features without needing to contribute to Wowchemy itself.

+ 4 - 5
.github/stale.yml

@@ -12,13 +12,12 @@ exemptLabels:
 # Label to use when marking an issue as stale
 staleLabel: stale
 # Comment to post when marking an issue as stale. Set to `false` to disable
-markComment: >
-  This issue has been automatically marked as stale because it has not had any
-  recent activity. The resources of the Academic team are limited, and so we are asking for your help.
+markComment: |
+  This issue has been automatically marked as stale because it has not had any recent activity. The resources of the project maintainers are limited, and so we are asking for your help.
 
-  If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, please reply with all of the information you have about it in order to keep the issue open.
+  If this is a **bug** and you can still reproduce this error on the <code>master</code> branch, consider contributing a Pull Request with a fix.
 
-  If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why.
+  If this is a **feature request**, and you feel that it is still relevant and valuable, please tell us why or contribute a Pull Request for review.
 
   This issue will automatically close soon if no further activity occurs. Thank you for your contributions.
 

+ 6 - 4
.github/support.md

@@ -1,10 +1,12 @@
-# How to get support for Academic 👨‍💬👩‍💬
+# How to get support for Wowchemy 👨‍💬👩‍💬
 
-For **help** and **questions** please join our **[community chat](https://spectrum.chat/academic)** or use the **[forum](https://discourse.gohugo.io/c/themes)** 🚑.
+🚑 For **help** and **questions**, solve common issues with the [**Troubleshooting Guide**](https://wowchemy.com/docs/faq/).
+
+For other issues, search if your question has already been asked on the community **[chat](https://discord.gg/z8wNYzb)**  and **[forum](https://github.com/wowchemy/wowchemy-hugo-modules/discussions)**, and if not, consider posting a new thread there.
 
 Please **_do not_** raise an issue on GitHub.
 
-Also, you can search and browse the extensive [Academic](https://sourcethemes.com/academic/docs/) and [Hugo](https://gohugo.io/documentation/) **documentation**.
+Also, you can search and browse the extensive [Wowchemy](https://wowchemy.com/docs/) and [Hugo](https://gohugo.io/documentation/) **documentation**.
 
 For questions on _Blogdown_, please reach out to the [Blogdown community](https://github.com/rstudio/blogdown).
 
@@ -14,4 +16,4 @@ Issues which are not bug reports or feature requests will be closed.
 
 GitHub is our project management tool, it's the place where our volunteers contribute. We use the issue list to keep track of bugs and new features that we are working on. We do this openly for transparency.
 
-With the chat and forum, you can leverage the knowledge of our wider community to get help with any problems you are having with Academic. Please keep in mind that Academic is Free Open Source Software (FOSS), and free support is provided by the goodwill of our wonderful community members.
+With the chat and forum, you can leverage the knowledge of our wider community to get help with any problems you are having with Wowchemy. Please keep in mind that Wowchemy is Free Open Source Software (FOSS), and free support is provided by the goodwill of our wonderful community members.

+ 1 - 1
LICENSE.md

@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2016-present George Cushen
+Copyright (c) 2016-present George Cushen (https://georgecushen.com/)
 
 Permission is hereby granted, free of charge, to any person obtaining a copy of
 this software and associated documentation files (the "Software"), to deal in

+ 4 - 0
netlify-cms-academic/config.yaml

@@ -0,0 +1,4 @@
+module:
+  mounts:
+    - source: static
+      target: static

+ 180 - 12
netlify-cms-academic/static/admin/config.yml

@@ -1,9 +1,77 @@
 backend:
   name: git-gateway
   branch: master
-media_folder: 'static/media/'
-public_folder: 'media'
+media_folder: 'static/media'
+public_folder: '/media'
 collections:
+  - name: home
+    label: "Homepage"
+    folder: 'content/home'
+    path: '{{slug}}'
+    # When specifying a path on a folder collection, media_folder defaults to an empty string, so make it explicit.
+    media_folder: '/static/media'
+    public_folder: ''
+    summary: "{{filename}}: {{title}}"
+    identifier_field: "widget_id"
+    create: true
+    fields:
+      - {label: "Widget Type (https://wowchemy.com/docs/page-builder/)", name: "widget", widget: "string", required: true}
+      - {label: 'Your reference for this widget (e.g. recent-posts)', name: 'widget_id', widget: 'string', default: 'my-widget-123'}
+      - {label: "Headless?", name: "headless", widget: "hidden", default: true}
+      - label: "Widget position"
+        name: "weight"
+        widget: "number"
+        default: 10
+        valueType: "int"
+        min: 0
+        max: 1001
+        step: 10
+      - {label: "Title", name: "title", widget: "string", required: false}
+      - {label: "Subtitle", name: "subtitle", widget: "string", required: false}
+      - label: "Enabled?"
+        name: "active"
+        required: false
+        widget: "boolean"
+        default: true
+      - label: "Widget Style"
+        name: "design"
+        widget: "object"
+        required: false
+        fields:
+          - {label: "Columns (options: `1` or `2`)", name: "columns", widget: "string", default: "2", required: false}
+          - label: "Background"
+            name: "background"
+            widget: "object"
+            required: false
+            fields:
+              - {label: 'Solid color', name: 'color', widget: 'color', enableAlpha: true, allowInput: true, required: false}
+              - {label: 'Gradient start', name: 'gradient_start', widget: 'color', enableAlpha: true, allowInput: true, required: false}
+              - {label: 'Gradient end', name: 'gradient_end', widget: 'color', enableAlpha: true, allowInput: true, required: false}
+              - label: "Use a light text color?"
+                name: "text_color_light"
+                required: false
+                widget: "boolean"
+                default: false
+              - label: "Image"
+                name: "image"
+                widget: "image"
+                required: false
+                # When specifying a path on a folder collection, media_folder defaults to an empty string, so make it explicit.
+                media_folder: '/static/media'
+                public_folder: ''
+                media_library:
+                  config:
+                    multiple: false
+              - label: "Darken the image? (0 is transparent & 1 is opaque)"
+                name: "image_darken"
+                widget: "number"
+                default: 0.0
+                valueType: "float"
+                min: 0.0
+                max: 1.0
+                step: 0.1
+                required: false
+      - {label: "Body", name: "body", widget: "markdown", required: false}
   - name: authors
     label: Authors
     label_singular: Author
@@ -43,7 +111,17 @@ collections:
               - {label: "Regular", value: "far"}
               - {label: "Brand", value: "fab"}
               - {label: "Academic", value: "ai"}
-          - {label: Icon (see https://sourcethemes.com/academic/docs/page-builder/#icons), name: icon, widget: string}
+          - {label: Icon (see https://wowchemy.com/docs/page-builder/#icons), name: icon, widget: string}
+          - {label: Label (tooltip), name: label, widget: string, required: false}
+          - label: Display in About widget and...
+            name: display
+            widget: object
+            fields:
+            - label: "Header (main menu)"
+              name: "header"
+              widget: "boolean"
+              default: false
+              required: false
       - label: "Organizations you belong to or are affiliated with (shown in About widget)"
         name: "organizations"
         required: false
@@ -89,10 +167,12 @@ collections:
         name: "draft"
         widget: "boolean"
         default: false
+        required: false
       - label: "Featured"
         name: "featured"
         widget: "boolean"
         default: false
+        required: false
       - label: "Authors"
         name: "authors"
         required: false
@@ -118,13 +198,14 @@ collections:
             name: "filename"
             widget: "image"
             default: "featured"
+            required: false
             media_library:
               config:
                 multiple: false
           - {label: Caption, name: caption, widget: string, required: false}
           - {label: Description for screen readers, name: alt_text, widget: string, required: false}
           - {label: "Where's the focal point in the image? Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight.", name: focal_point, widget: string, required: false, default: "Smart"}
-          - {label: Thumbnail Only?, name: preview_only, widget: boolean, default: false}
+          - {label: Thumbnail Only?, name: preview_only, widget: boolean, default: false, required: false}
   - name: projects
     label: Projects
     label_singular: Project
@@ -141,10 +222,12 @@ collections:
         name: "draft"
         widget: "boolean"
         default: false
+        required: false
       - label: "Featured"
         name: "featured"
         widget: "boolean"
         default: false
+        required: false
       - label: "Authors"
         name: "authors"
         required: false
@@ -176,7 +259,7 @@ collections:
               - {label: "Regular", value: "far"}
               - {label: "Brand", value: "fab"}
               - {label: "Academic", value: "ai"}
-          - {label: "Icon (see https://sourcethemes.com/academic/docs/page-builder/#icons)", name: icon, widget: string, required: false}
+          - {label: "Icon (see https://wowchemy.com/docs/page-builder/#icons)", name: icon, widget: string, required: false}
       - label: "Featured Image"
         name: "image"
         required: false
@@ -186,17 +269,18 @@ collections:
             name: "filename"
             widget: "image"
             default: "featured"
+            required: false
             media_library:
               config:
                 multiple: false
           - {label: Caption, name: caption, widget: string, required: false}
           - {label: Description for screen readers, name: alt_text, widget: string, required: false}
           - {label: "Where's the focal point in the image? Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight.", name: focal_point, widget: string, required: false, default: "Smart"}
-          - {label: Thumbnail Only?, name: preview_only, widget: boolean, default: false}
-  - name: talks
-    label: Talks
-    label_singular: Talk
-    folder: 'content/talk'
+          - {label: Thumbnail Only?, name: preview_only, widget: boolean, default: false, required: false}
+  - name: events
+    label: Events
+    label_singular: Event
+    folder: 'content/event'
     path: '{{slug}}/index'
     create: true  # Allow users to create new documents in this collection
     fields:  # The fields each document in this collection have
@@ -224,7 +308,7 @@ collections:
               - {label: "Regular", value: "far"}
               - {label: "Brand", value: "fab"}
               - {label: "Academic", value: "ai"}
-          - {label: "Icon (see https://sourcethemes.com/academic/docs/page-builder/#icons)", name: icon, widget: string, required: false}
+          - {label: "Icon (see https://wowchemy.com/docs/page-builder/#icons)", name: icon, widget: string, required: false}
       - {label: "Event", name: "event", widget: "string"}
       - {label: "Event link", name: "event_url", widget: "string"}
       - {label: "Publish this page on", name: "publishDate", widget: "datetime"}
@@ -233,10 +317,12 @@ collections:
         name: "draft"
         widget: "boolean"
         default: false
+        required: false
       - label: "Featured"
         name: "featured"
         widget: "boolean"
         default: false
+        required: false
       - label: "Authors"
         name: "authors"
         required: false
@@ -262,14 +348,96 @@ collections:
             name: "filename"
             widget: "image"
             default: "featured"
+            required: false
             media_library:
               config:
                 multiple: false
           - {label: Caption, name: caption, widget: string, required: false}
           - {label: Description for screen readers, name: alt_text, widget: string, required: false}
           - {label: "Where's the focal point in the image? Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight.", name: focal_point, widget: string, required: false, default: "Smart"}
-          - {label: Thumbnail Only?, name: preview_only, widget: boolean, default: false}
+          - {label: Thumbnail Only?, name: preview_only, widget: boolean, default: false, required: false}
+      - {label: "Details", name: "body", widget: "markdown", required: false}
+  - name: publications
+    label: Publications
+    label_singular: Publication
+    folder: 'content/publication'
+    path: '{{slug}}/index'
+    create: true  # Allow users to create new documents in this collection
+    fields: # The fields each document in this collection have
+      - { label: "Title", name: "title", widget: "string" }
+      - { label: "Subtitle", name: "subtitle", widget: "string", required: false }
+      - label: "Publication type"
+        name: "publication_types"
+        required: true
+        default: ["0"]
+        widget: "select"
+        # Can only have 1 pub. type assigned, but need `multiple` option to save as a Hugo taxonomy list.
+        multiple: true
+        options:
+          - { label: "Uncategorized", value: "0" }
+          - { label: "Conference paper", value: "1" }
+          - { label: "Journal article", value: "2" }
+          - { label: "Preprint / Working Paper", value: "3" }
+          - { label: "Report", value: "4" }
+          - { label: "Book", value: "5" }
+          - { label: "Book section", value: "6" }
+          - { label: "Thesis", value: "7" }
+          - { label: "Patent", value: "8" }
+      - label: "Authors"
+        name: "authors"
+        required: true
+        widget: "list"
+      - label: "Author Notes (contributions or affiliations for each author)"
+        name: "author_notes"
+        required: false
+        widget: "list"
+      - { label: "DOI", name: "doi", widget: "string", required: false }
+      - { label: "Publication", name: "publication", widget: "string", required: false }
+      - { label: "Publication (abbreviated)", name: "publication_short", widget: "string", required: false }
+      - { label: "Abstract", name: "abstract", widget: "text", required: false }
+      - label: "Draft"
+        name: "draft"
+        widget: "boolean"
+        default: false
+        required: false
+      - label: "Featured"
+        name: "featured"
+        widget: "boolean"
+        default: false
+        required: false
+      - label: "Tags"
+        name: "tags"
+        required: false
+        widget: "list"
+      - label: "Categories"
+        name: "categories"
+        required: false
+        widget: "list"
+      - label: "Projects"
+        name: "projects"
+        required: false
+        widget: "list"
+      - {label: "Markdown slides (reference a deck in 'content/slides/')", name: "slides", widget: "string", required: false}
+      - label: "Featured Image"
+        name: "image"
+        required: false
+        widget: object
+        fields:
+          - label: "Upload an image named `featured.jpg/png`"
+            name: "filename"
+            widget: "image"
+            default: "featured"
+            required: false
+            media_library:
+              config:
+                multiple: false
+          - { label: Caption, name: caption, widget: string, required: false }
+          - { label: Description for screen readers, name: alt_text, widget: string, required: false }
+          - { label: "Where's the focal point in the image? Smart, Center, TopLeft, Top, TopRight, Left, Right, BottomLeft, Bottom, BottomRight.", name: focal_point, widget: string, required: false, default: "Smart" }
+          - { label: Thumbnail Only?, name: preview_only, widget: boolean, default: false, required: false }
+      - { label: "Summary (shortened abstract)", name: "summary", widget: "text", required: false }
       - {label: "Details", name: "body", widget: "markdown", required: false}
+      - { label: "Publish this page on", name: "date", widget: "datetime" }
   - name: pages
     label: "Pages"
     files:

+ 1 - 1
scripts/list_language_packs.py

@@ -9,7 +9,7 @@
 import yaml
 from pathlib import Path
 
-LANG_PATH = Path(__file__).resolve().parent.parent.joinpath('data').joinpath('i18n')
+LANG_PATH = Path(__file__).resolve().parent.parent.joinpath('wowchemy').joinpath('data').joinpath('i18n')
 LANG_YAML = LANG_PATH.joinpath('languages.yaml')
 
 

+ 11 - 0
themes/hello-world/assets/scss/template.scss

@@ -0,0 +1,11 @@
+// Narrower container for Hello World sections (primarily Blank widget content).
+@media (min-width: 1200px) {
+  .home-section .container {
+    max-width: 880px;
+  }
+}
+@media (min-width: 992px){
+  .home-section .container {
+    max-width: 880px;
+  }
+}

+ 16 - 0
themes/hello-world/config.yaml

@@ -0,0 +1,16 @@
+module:
+  mounts:
+    - source: content
+      target: content
+    - source: static
+      target: static
+    - source: layouts
+      target: layouts
+    - source: data
+      target: data
+    - source: assets
+      target: assets
+    - source: i18n
+      target: i18n
+    - source: archetypes
+      target: archetypes

+ 3 - 0
themes/hello-world/go.mod

@@ -0,0 +1,3 @@
+module github.com/wowchemy/wowchemy-hugo-modules/themes/hello-world
+
+go 1.15

+ 4 - 4
wowchemy/archetypes/talk/index.md → wowchemy/archetypes/event/index.md

@@ -20,13 +20,13 @@ date: {{ .Date }}
 date_end: {{ .Date }}
 all_day: false
 
-# Schedule page publish date (NOT talk date).
+# Schedule page publish date (NOT event date).
 publishDate: {{ .Date }}
 
 authors: []
 tags: []
 
-# Is this a featured talk? (true/false)
+# Is this a featured event? (true/false)
 featured: false
 
 # Featured image
@@ -45,7 +45,7 @@ image:
 #   icon_pack: fab
 #   icon: twitter
 
-# Optional filename of your slides within your talk's folder or a URL.
+# Optional filename of your slides within your event's folder or a URL.
 url_slides:
 
 url_code:
@@ -53,7 +53,7 @@ url_pdf:
 url_video:
 
 # Markdown Slides (optional).
-#   Associate this talk with Markdown slides.
+#   Associate this event with Markdown slides.
 #   Simply enter your slide deck's filename without extension.
 #   E.g. `slides = "example-slides"` references `content/slides/example-slides.md`.
 #   Otherwise, set `slides = ""`.

BIN
wowchemy/assets/images/icon.png


+ 0 - 47
wowchemy/assets/js/load-theme.js

@@ -1,47 +0,0 @@
-(function () {
-  function getThemeMode() {
-    return parseInt(localStorage.getItem('dark_mode') || 2);
-  }
-
-  function canChangeTheme() {
-    // If var is set, then user is allowed to change the theme variation.
-    return Boolean(window.wcDarkLightEnabled);
-  }
-
-  function initThemeVariation() {
-    if (!canChangeTheme()) {
-      return;
-    }
-
-    let currentThemeMode = getThemeMode();
-    let isDarkTheme;
-    switch (currentThemeMode) {
-      case 0:
-        isDarkTheme = false;
-        break;
-      case 1:
-        isDarkTheme = true;
-        break;
-      default:
-        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
-          // The visitor prefers dark themes and switching to the dark variation is allowed by admin.
-          isDarkTheme = true;
-        } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
-          // The visitor prefers light themes and switching to the dark variation is allowed by admin.
-          isDarkTheme = false;
-        } else {
-          // Use the site's default theme variation based on `light` in the theme file.
-          isDarkTheme = isSiteThemeDark;
-        }
-        break;
-    }
-    if (isDarkTheme) {
-      document.body.classList.add("dark");
-    } else {
-      document.body.classList.remove("dark");
-    }
-  }
-
-  // Initialize theme variation.
-  initThemeVariation();
-})();

+ 24 - 0
wowchemy/assets/js/wowchemy-animation.js

@@ -0,0 +1,24 @@
+/*************************************************
+ *  Wowchemy
+ *  https://github.com/wowchemy/wowchemy-hugo-modules
+ *
+ *  Wowchemy Animation
+ **************************************************/
+
+function fadeIn(element, duration = 600) {
+  element.style.display = '';
+  element.style.opacity = '0';
+  let last = +new Date();
+  let tick = function() {
+    element.style.opacity = (+element.style.opacity + (new Date() - last) / duration).toString();
+    last = +new Date();
+    if (+element.style.opacity < 1) {
+      (window.requestAnimationFrame && requestAnimationFrame(tick)) || setTimeout(tick, 16);
+    }
+  };
+  tick();
+}
+
+export {
+  fadeIn,
+};

+ 18 - 0
wowchemy/assets/js/wowchemy-init.js

@@ -0,0 +1,18 @@
+/*************************************************
+ *  Wowchemy
+ *  https://github.com/wowchemy/wowchemy-hugo-modules
+ *
+ *  Wowchemy Initialization
+ **************************************************/
+
+import {initThemeVariation} from './wowchemy-theming';
+
+import {wcDarkLightEnabled, wcIsSiteThemeDark} from '@params';
+
+window.wc = {
+  darkLightEnabled: wcDarkLightEnabled,
+  isSiteThemeDark: wcIsSiteThemeDark,
+}
+
+// Initialize theme variation and set body theme class.
+initThemeVariation();

+ 3 - 1
wowchemy/assets/js/wowchemy-search.js

@@ -55,6 +55,7 @@ function initSearch(force, fuse) {
   // If query deleted, clear results.
   if ( query.length < 1) {
     $('#search-hits').empty();
+    $('#search-common-queries').show();
   }
 
   // Check for timer event (enter key not pressed) and query less than minimum length required.
@@ -63,6 +64,7 @@ function initSearch(force, fuse) {
 
   // Do search.
   $('#search-hits').empty();
+  $('#search-common-queries').hide();
   searchAcademic(query, fuse);
   let newURL = window.location.protocol + "//" + window.location.host + window.location.pathname + '?q=' + encodeURIComponent(query) + window.location.hash;
   updateURL(newURL);
@@ -90,7 +92,7 @@ function parseResults(query, results) {
     let snippetHighlights = [];
 
     // Show abstract in results for content types where the abstract is often the primary content.
-    if (["publication", "talk"].includes(content_key)) {
+    if (["publication", "event"].includes(content_key)) {
       content = value.item.summary;
     } else {
       content = value.item.content;

+ 255 - 0
wowchemy/assets/js/wowchemy-theming.js

@@ -0,0 +1,255 @@
+/*************************************************
+ *  Wowchemy
+ *  https://github.com/wowchemy/wowchemy-hugo-modules
+ *
+ *  Wowchemy Theming System
+ *  Supported Modes: {0: Light, 1: Dark, 2: Auto}
+ **************************************************/
+
+import {fadeIn} from './wowchemy-animation';
+
+const body = document.body;
+
+function getThemeMode() {
+  return parseInt(localStorage.getItem('wcTheme') || 2);
+}
+
+function canChangeTheme() {
+  // If var is set, then user is allowed to change the theme variation.
+  return Boolean(window.wc.darkLightEnabled);
+}
+
+// initThemeVariation is first called directly after <body> to prevent
+// flashing between the default theme mode and the user's choice.
+function initThemeVariation() {
+  if (!canChangeTheme()) {
+    return {
+      isDarkTheme: window.wc.isSiteThemeDark,
+      themeMode: window.wc.isSiteThemeDark ? 1 : 0,
+    };
+  }
+
+  let currentThemeMode = getThemeMode();
+  let isDarkTheme;
+
+  switch (currentThemeMode) {
+    case 0:
+      isDarkTheme = false;
+      break;
+    case 1:
+      isDarkTheme = true;
+      break;
+    default:
+      if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+        // The visitor prefers dark themes and switching to the dark variation is allowed by admin.
+        isDarkTheme = true;
+      } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
+        // The visitor prefers light themes and switching to the dark variation is allowed by admin.
+        isDarkTheme = false;
+      } else {
+        // Use the site's default theme variation based on `light` in the theme file.
+        isDarkTheme = window.wc.isSiteThemeDark;
+      }
+      break;
+  }
+
+  if (isDarkTheme && !body.classList.contains('dark')) {
+    console.debug('Applying Wowchemy dark theme');
+    document.body.classList.add("dark");
+  } else if (body.classList.contains('dark')) {
+    console.debug('Applying Wowchemy light theme');
+    document.body.classList.remove("dark");
+  }
+
+  return {
+    isDarkTheme: isDarkTheme,
+    themeMode: currentThemeMode,
+  };
+}
+
+function changeThemeModeClick(newMode) {
+  if (!canChangeTheme()) {
+    console.info('Cannot set theme - admin disabled theme selector.');
+    return;
+  }
+  let isDarkTheme;
+  switch (newMode) {
+    case 0:
+      localStorage.setItem('wcTheme', '0');
+      isDarkTheme = false;
+      console.info('User changed theme variation to Light.');
+      break;
+    case 1:
+      localStorage.setItem('wcTheme', '1');
+      isDarkTheme = true;
+      console.info('User changed theme variation to Dark.');
+      break;
+    default:
+      localStorage.setItem('wcTheme', '2');
+      if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+        // The visitor prefers dark themes and switching to the dark variation is allowed by admin.
+        isDarkTheme = true;
+      } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
+        // The visitor prefers light themes and switching to the dark variation is allowed by admin.
+        isDarkTheme = false;
+      } else {
+        // Use the site's default theme variation based on `light` in the theme file.
+        isDarkTheme = window.wc.isSiteThemeDark;
+      }
+      console.info('User changed theme variation to Auto.');
+      break;
+  }
+  renderThemeVariation(isDarkTheme, newMode);
+}
+
+function showActiveTheme(mode) {
+  let linkLight = document.querySelector('.js-set-theme-light');
+  let linkDark = document.querySelector('.js-set-theme-dark');
+  let linkAuto = document.querySelector('.js-set-theme-auto');
+
+  if (linkLight === null) {
+    return;
+  }
+
+  switch (mode) {
+    case 0:
+      // Light.
+      linkLight.classList.add('dropdown-item-active');
+      linkDark.classList.remove('dropdown-item-active');
+      linkAuto.classList.remove('dropdown-item-active');
+      break;
+    case 1:
+      // Dark.
+      linkLight.classList.remove('dropdown-item-active');
+      linkDark.classList.add('dropdown-item-active');
+      linkAuto.classList.remove('dropdown-item-active');
+      break;
+    default:
+      // Auto.
+      linkLight.classList.remove('dropdown-item-active');
+      linkDark.classList.remove('dropdown-item-active');
+      linkAuto.classList.add('dropdown-item-active');
+      break;
+  }
+}
+
+/**
+ * Render theme variation (day or night).
+ *
+ * @param {boolean} isDarkTheme
+ * @param {int} themeMode - {0: Light, 1: Dark, 2: Auto}
+ * @param {boolean} init - true only when called on document ready
+ * @returns {undefined}
+ */
+function renderThemeVariation(isDarkTheme, themeMode = 2, init = false) {
+  // Is code highlighting enabled in site config?
+  const codeHlLight = document.querySelector('link[title=hl-light]');
+  const codeHlDark = document.querySelector('link[title=hl-dark]');
+  const codeHlEnabled = (codeHlLight !== null) || (codeHlDark !== null);
+  const diagramEnabled = document.querySelector('script[title=mermaid]') !== null;
+
+  // Update active theme mode in navbar theme selector.
+  showActiveTheme(themeMode);
+
+  // Check if re-render required.
+  if (!init) {
+    // If request to render light when light variation already rendered, return.
+    // If request to render dark when dark variation already rendered, return.
+    if ((isDarkTheme === false && !body.classList.contains('dark')) || (isDarkTheme === true && body.classList.contains('dark'))) {
+      return;
+    }
+  }
+
+  if (isDarkTheme === false) {
+    if (!init) {
+      // Only fade in the page when changing the theme variation.
+      Object.assign(document.body.style, {opacity: 0, visibility: 'visible'});
+      fadeIn(document.body, 600);
+    }
+    body.classList.remove('dark');
+    if (codeHlEnabled) {
+      console.debug('Setting HLJS theme to light');
+      if (codeHlLight) {
+        codeHlLight.disabled = false;
+      }
+      if (codeHlDark){
+        codeHlDark.disabled = true;
+      }
+    }
+    if (diagramEnabled) {
+      console.debug('Initializing Mermaid with light theme');
+      if (init) {
+        /** @namespace window.mermaid **/
+        window.mermaid.initialize({theme: 'default', securityLevel: 'loose'});
+      } else {
+        // Have to reload to re-initialise Mermaid with the new theme and re-parse the Mermaid code blocks.
+        location.reload();
+      }
+    }
+  } else if (isDarkTheme === true) {
+    if (!init) {
+      // Only fade in the page when changing the theme variation.
+      Object.assign(document.body.style, {opacity: 0, visibility: 'visible'});
+      fadeIn(document.body, 600);
+    }
+    body.classList.add("dark");
+    if (codeHlEnabled) {
+      console.debug('Setting HLJS theme to dark');
+      if (codeHlLight) {
+        codeHlLight.disabled = true;
+      }
+      if (codeHlDark){
+        codeHlDark.disabled = false;
+      }
+    }
+    if (diagramEnabled) {
+      console.debug('Initializing Mermaid with dark theme');
+      if (init) {
+        /** @namespace window.mermaid **/
+        window.mermaid.initialize({theme: 'dark', securityLevel: 'loose'});
+      } else {
+        // Have to reload to re-initialise Mermaid with the new theme and re-parse the Mermaid code blocks.
+        location.reload();
+      }
+    }
+  }
+}
+
+/**
+ * onMediaQueryListEvent.
+ *
+ * @param {MediaQueryListEvent} event
+ * @returns {undefined}
+ */
+function onMediaQueryListEvent(event) {
+  if (!canChangeTheme()) {
+    // Changing theme variation is not allowed by admin.
+    return;
+  }
+  const darkModeOn = event.matches;
+  console.debug(`OS dark mode preference changed to ${darkModeOn ? '🌒 on' : '☀️ off'}.`);
+  let currentThemeVariation = getThemeMode();
+  let isDarkTheme;
+  if (currentThemeVariation === 2) {
+    if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
+      // The visitor prefers dark themes.
+      isDarkTheme = true;
+    } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
+      // The visitor prefers light themes.
+      isDarkTheme = false;
+    } else {
+      // The visitor does not have a day or night preference, so use the theme's default setting.
+      isDarkTheme = window.wc.isSiteThemeDark;
+    }
+    renderThemeVariation(isDarkTheme, currentThemeVariation);
+  }
+}
+
+export {
+  canChangeTheme,
+  initThemeVariation,
+  changeThemeModeClick,
+  renderThemeVariation,
+  getThemeMode,
+  onMediaQueryListEvent,
+};

+ 28 - 0
wowchemy/assets/js/wowchemy-utils.js

@@ -0,0 +1,28 @@
+/*************************************************
+ *  Wowchemy
+ *  https://github.com/wowchemy/wowchemy-hugo-modules
+ *
+ *  Wowchemy Utilities
+ **************************************************/
+
+/**
+ * Fix Mermaid.js clash with Highlight.js.
+ * Refactor Mermaid code blocks as divs to prevent Highlight parsing them and enable Mermaid to parse them.
+ */
+function fixMermaid() {
+  let mermaids = [];
+  // Note that `language-mermaid` class is applied to <code> block within <pre>, so we wish to replace parent node.
+  [].push.apply(mermaids, document.getElementsByClassName('language-mermaid'));
+  for (let i = 0; i < mermaids.length; i++) {
+    // Convert <pre><code></code></pre> block to <div> and add `mermaid` class so that Mermaid will parse it.
+    let mermaidCodeElement = mermaids[i];
+    let newElement = document.createElement('div');
+    newElement.innerHTML = mermaidCodeElement.innerHTML;
+    newElement.classList.add('mermaid');
+    mermaidCodeElement.parentNode.replaceWith(newElement);
+  }
+}
+
+export {
+  fixMermaid,
+};

+ 551 - 716
wowchemy/assets/js/wowchemy.js

@@ -5,806 +5,641 @@
  *  Core JS functions and initialization.
  **************************************************/
 
-(function ($) {
-
-  /* ---------------------------------------------------------------------------
-   * Responsive scrolling for URL hashes.
-   * --------------------------------------------------------------------------- */
-
-  // Dynamically get responsive navigation bar height for offsetting Scrollspy.
-  function getNavBarHeight() {
-    let $navbar = $('#navbar-main');
-    let navbar_offset = $navbar.outerHeight();
-    console.debug('Navbar height: ' + navbar_offset);
-    return navbar_offset;
-  }
-
-  /**
-   * Responsive hash scrolling.
-   * Check for a URL hash as an anchor.
-   * If it exists on current page, scroll to it responsively.
-   * If `target` argument omitted (e.g. after event), assume it's the window's hash.
-   */
-  function scrollToAnchor(target) {
-    // If `target` is undefined or HashChangeEvent object, set it to window's hash.
-    // Decode the hash as browsers can encode non-ASCII characters (e.g. Chinese symbols).
-    target = (typeof target === 'undefined' || typeof target === 'object') ? decodeURIComponent(window.location.hash) : target;
-
-    // If target element exists, scroll to it taking into account fixed navigation bar offset.
-    if ($(target).length) {
-      // Escape special chars from IDs, such as colons found in Markdown footnote links.
-      target = '#' + $.escapeSelector(target.substring(1));  // Previously, `target = target.replace(/:/g, '\\:');`
-
-      let elementOffset = Math.ceil($(target).offset().top - getNavBarHeight());  // Round up to highlight right ID!
-      $('body').addClass('scrolling');
-      $('html, body').animate({
-        scrollTop: elementOffset
-      }, 600, function () {
-        $('body').removeClass('scrolling');
-      });
-    } else {
-      console.debug('Cannot scroll to target `#' + target + '`. ID not found!');
-    }
-  }
+import {hugoEnvironment} from '@params';
 
-  // Make Scrollspy responsive.
-  function fixScrollspy() {
-    let $body = $('body');
-    let data = $body.data('bs.scrollspy');
-    if (data) {
-      data._config.offset = getNavBarHeight();
-      $body.data('bs.scrollspy', data);
-      $body.scrollspy('refresh');
-    }
-  }
+import {fixMermaid} from './wowchemy-utils';
 
-  function removeQueryParamsFromUrl() {
-    if (window.history.replaceState) {
-      let urlWithoutSearchParams = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.hash;
-      window.history.replaceState({path: urlWithoutSearchParams}, '', urlWithoutSearchParams);
-    }
-  }
+import {
+  changeThemeModeClick,
+  initThemeVariation,
+  renderThemeVariation,
+  onMediaQueryListEvent,
+} from './wowchemy-theming';
 
-  // Check for hash change event and fix responsive offset for hash links (e.g. Markdown footnotes).
-  window.addEventListener("hashchange", scrollToAnchor);
+console.debug(`Environment: ${hugoEnvironment}`)
 
-  /* ---------------------------------------------------------------------------
-   * Add smooth scrolling to all links inside the main navbar.
-   * --------------------------------------------------------------------------- */
-
-  $('#navbar-main li.nav-item a.nav-link, .js-scroll').on('click', function (event) {
-    // Store requested URL hash.
-    let hash = this.hash;
+/* ---------------------------------------------------------------------------
+ * Responsive scrolling for URL hashes.
+ * --------------------------------------------------------------------------- */
 
-    // If we are on a widget page and the navbar link is to a section on the same page.
-    if (this.pathname === window.location.pathname && hash && $(hash).length && ($(".js-widget-page").length > 0)) {
-      // Prevent default click behavior.
-      event.preventDefault();
+// Dynamically get responsive navigation bar height for offsetting Scrollspy.
+function getNavBarHeight() {
+  let $navbar = $('#navbar-main');
+  let navbar_offset = $navbar.outerHeight();
+  console.debug('Navbar height: ' + navbar_offset);
+  return navbar_offset;
+}
+
+/**
+ * Responsive hash scrolling.
+ * Check for a URL hash as an anchor.
+ * If it exists on current page, scroll to it responsively.
+ * If `target` argument omitted (e.g. after event), assume it's the window's hash.
+ */
+function scrollToAnchor(target, duration = 600) {
+  // If `target` is undefined or HashChangeEvent object, set it to window's hash.
+  // Decode the hash as browsers can encode non-ASCII characters (e.g. Chinese symbols).
+  target = (typeof target === 'undefined' || typeof target === 'object') ? decodeURIComponent(window.location.hash) : target;
+
+  // If target element exists, scroll to it taking into account fixed navigation bar offset.
+  if ($(target).length) {
+    // Escape special chars from IDs, such as colons found in Markdown footnote links.
+    target = '#' + $.escapeSelector(target.substring(1));  // Previously, `target = target.replace(/:/g, '\\:');`
+
+    let elementOffset = Math.ceil($(target).offset().top - getNavBarHeight());  // Round up to highlight right ID!
+    $('body').addClass('scrolling');
+    $('html, body').animate({
+      scrollTop: elementOffset
+    }, duration, function () {
+      $('body').removeClass('scrolling');
+    });
+  } else {
+    console.debug('Cannot scroll to target `#' + target + '`. ID not found!');
+  }
+}
+
+// Make Scrollspy responsive.
+function fixScrollspy() {
+  let $body = $('body');
+  let data = $body.data('bs.scrollspy');
+  if (data) {
+    data._config.offset = getNavBarHeight();
+    $body.data('bs.scrollspy', data);
+    $body.scrollspy('refresh');
+  }
+}
 
-      // Use jQuery's animate() method for smooth page scrolling.
-      // The numerical parameter specifies the time (ms) taken to scroll to the specified hash.
-      let elementOffset = Math.ceil($(hash).offset().top - getNavBarHeight());  // Round up to highlight right ID!
+function removeQueryParamsFromUrl() {
+  if (window.history.replaceState) {
+    let urlWithoutSearchParams = window.location.protocol + "//" + window.location.host + window.location.pathname + window.location.hash;
+    window.history.replaceState({path: urlWithoutSearchParams}, '', urlWithoutSearchParams);
+  }
+}
 
-      // Uncomment to debug.
-      // let scrollTop = $(window).scrollTop();
-      // let scrollDelta = (elementOffset - scrollTop);
-      // console.debug('Scroll Delta: ' + scrollDelta);
+// Check for hash change event and fix responsive offset for hash links (e.g. Markdown footnotes).
+window.addEventListener("hashchange", scrollToAnchor);
 
-      $('html, body').animate({
-        scrollTop: elementOffset
-      }, 800);
-    }
-  });
+/* ---------------------------------------------------------------------------
+ * Add smooth scrolling to all links inside the main navbar.
+ * --------------------------------------------------------------------------- */
 
-  /* ---------------------------------------------------------------------------
-   * Hide mobile collapsable menu on clicking a link.
-   * --------------------------------------------------------------------------- */
+$('#navbar-main li.nav-item a.nav-link, .js-scroll').on('click', function (event) {
+  // Store requested URL hash.
+  let hash = this.hash;
 
-  $(document).on('click', '.navbar-collapse.show', function (e) {
-    //get the <a> element that was clicked, even if the <span> element that is inside the <a> element is e.target
-    let targetElement = $(e.target).is('a') ? $(e.target) : $(e.target).parent();
+  // If we are on a widget page and the navbar link is to a section on the same page.
+  if (this.pathname === window.location.pathname && hash && $(hash).length && ($(".js-widget-page").length > 0)) {
+    // Prevent default click behavior.
+    event.preventDefault();
 
-    if (targetElement.is('a') && targetElement.attr('class') != 'dropdown-toggle') {
-      $(this).collapse('hide');
-    }
-  });
+    // Use jQuery's animate() method for smooth page scrolling.
+    // The numerical parameter specifies the time (ms) taken to scroll to the specified hash.
+    let elementOffset = Math.ceil($(hash).offset().top - getNavBarHeight());  // Round up to highlight right ID!
 
-  /* ---------------------------------------------------------------------------
-   * Filter publications.
-   * --------------------------------------------------------------------------- */
-
-  // Active publication filters.
-  let pubFilters = {};
-
-  // Search term.
-  let searchRegex;
-
-  // Filter values (concatenated).
-  let filterValues;
-
-  // Publication container.
-  let $grid_pubs = $('#container-publications');
-
-  // Initialise Isotope publication layout if required.
-  if ($grid_pubs.length)
-  {
-    $grid_pubs.isotope({
-      itemSelector: '.isotope-item',
-      percentPosition: true,
-      masonry: {
-        // Use Bootstrap compatible grid layout.
-        columnWidth: '.grid-sizer'
-      },
-      filter: function () {
-        let $this = $(this);
-        let searchResults = searchRegex ? $this.text().match(searchRegex) : true;
-        let filterResults = filterValues ? $this.is(filterValues) : true;
-        return searchResults && filterResults;
-      }
-    });
+    // Uncomment to debug.
+    // let scrollTop = $(window).scrollTop();
+    // let scrollDelta = (elementOffset - scrollTop);
+    // console.debug('Scroll Delta: ' + scrollDelta);
 
-    // Filter by search term.
-    let $quickSearch = $('.filter-search').keyup(debounce(function () {
-      searchRegex = new RegExp($quickSearch.val(), 'gi');
-      $grid_pubs.isotope();
-    }));
+    $('html, body').animate({
+      scrollTop: elementOffset
+    }, 800);
+  }
+});
 
-    $('.pub-filters').on('change', function () {
-      let $this = $(this);
+/* ---------------------------------------------------------------------------
+ * Hide mobile collapsable menu on clicking a link.
+ * --------------------------------------------------------------------------- */
 
-      // Get group key.
-      let filterGroup = $this[0].getAttribute('data-filter-group');
+$(document).on('click', '.navbar-collapse.show', function (e) {
+  //get the <a> element that was clicked, even if the <span> element that is inside the <a> element is e.target
+  let targetElement = $(e.target).is('a') ? $(e.target) : $(e.target).parent();
 
-      // Set filter for group.
-      pubFilters[filterGroup] = this.value;
+  if (targetElement.is('a') && targetElement.attr('class') != 'dropdown-toggle') {
+    $(this).collapse('hide');
+  }
+});
 
-      // Combine filters.
-      filterValues = concatValues(pubFilters);
+/* ---------------------------------------------------------------------------
+ * Filter publications.
+ * --------------------------------------------------------------------------- */
 
-      // Activate filters.
-      $grid_pubs.isotope();
+// Active publication filters.
+let pubFilters = {};
 
-      // If filtering by publication type, update the URL hash to enable direct linking to results.
-      if (filterGroup === "pubtype") {
-        // Set hash URL to current filter.
-        let url = $(this).val();
-        if (url.substr(0, 9) === '.pubtype-') {
-          window.location.hash = url.substr(9);
-        } else {
-          window.location.hash = '';
-        }
-      }
-    });
-  }
+// Search term.
+let searchRegex;
 
-  // Debounce input to prevent spamming filter requests.
-  function debounce(fn, threshold) {
-    let timeout;
-    threshold = threshold || 100;
-    return function debounced() {
-      clearTimeout(timeout);
-      let args = arguments;
-      let _this = this;
-
-      function delayed() {
-        fn.apply(_this, args);
-      }
+// Filter values (concatenated).
+let filterValues;
 
-      timeout = setTimeout(delayed, threshold);
-    };
-  }
+// Publication container.
+let $grid_pubs = $('#container-publications');
 
-  // Flatten object by concatenating values.
-  function concatValues(obj) {
-    let value = '';
-    for (let prop in obj) {
-      value += obj[prop];
+// Initialise Isotope publication layout if required.
+if ($grid_pubs.length) {
+  $grid_pubs.isotope({
+    itemSelector: '.isotope-item',
+    percentPosition: true,
+    masonry: {
+      // Use Bootstrap compatible grid layout.
+      columnWidth: '.grid-sizer'
+    },
+    filter: function () {
+      let $this = $(this);
+      let searchResults = searchRegex ? $this.text().match(searchRegex) : true;
+      let filterResults = filterValues ? $this.is(filterValues) : true;
+      return searchResults && filterResults;
     }
-    return value;
-  }
+  });
 
-  // Filter publications according to hash in URL.
-  function filter_publications() {
-    // Check for Isotope publication layout.
-    if (!$grid_pubs.length)
-      return
+  // Filter by search term.
+  let $quickSearch = $('.filter-search').keyup(debounce(function () {
+    searchRegex = new RegExp($quickSearch.val(), 'gi');
+    $grid_pubs.isotope();
+  }));
 
-    let urlHash = window.location.hash.replace('#', '');
-    let filterValue = '*';
+  $('.pub-filters').on('change', function () {
+    let $this = $(this);
 
-    // Check if hash is numeric.
-    if (urlHash != '' && !isNaN(urlHash)) {
-      filterValue = '.pubtype-' + urlHash;
-    }
+    // Get group key.
+    let filterGroup = $this[0].getAttribute('data-filter-group');
 
-    // Set filter.
-    let filterGroup = 'pubtype';
-    pubFilters[filterGroup] = filterValue;
+    // Set filter for group.
+    pubFilters[filterGroup] = this.value;
+
+    // Combine filters.
     filterValues = concatValues(pubFilters);
 
     // Activate filters.
     $grid_pubs.isotope();
 
-    // Set selected option.
-    $('.pubtype-select').val(filterValue);
-  }
-
-  /* ---------------------------------------------------------------------------
-  * Google Maps or OpenStreetMap via Leaflet.
-  * --------------------------------------------------------------------------- */
-
-  function initMap() {
-    if ($('#map').length) {
-      let map_provider = $('#map-provider').val();
-      let lat = $('#map-lat').val();
-      let lng = $('#map-lng').val();
-      let zoom = parseInt($('#map-zoom').val());
-      let address = $('#map-dir').val();
-      let api_key = $('#map-api-key').val();
-
-      if (map_provider == 1) {
-        let map = new GMaps({
-          div: '#map',
-          lat: lat,
-          lng: lng,
-          zoom: zoom,
-          zoomControl: true,
-          zoomControlOpt: {
-            style: 'SMALL',
-            position: 'TOP_LEFT'
-          },
-          panControl: false,
-          streetViewControl: false,
-          mapTypeControl: false,
-          overviewMapControl: false,
-          scrollwheel: true,
-          draggable: true
-        });
-
-        map.addMarker({
-          lat: lat,
-          lng: lng,
-          click: function (e) {
-            let url = 'https://www.google.com/maps/place/' + encodeURIComponent(address) + '/@' + lat + ',' + lng + '/';
-            window.open(url, '_blank')
-          },
-          title: address
-        })
+    // If filtering by publication type, update the URL hash to enable direct linking to results.
+    if (filterGroup === "pubtype") {
+      // Set hash URL to current filter.
+      let url = $(this).val();
+      if (url.substr(0, 9) === '.pubtype-') {
+        window.location.hash = url.substr(9);
       } else {
-        let map = new L.map('map').setView([lat, lng], zoom);
-        if (map_provider == 3 && api_key.length) {
-          L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
-            attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
-            tileSize: 512,
-            maxZoom: 18,
-            zoomOffset: -1,
-            id: 'mapbox/streets-v11',
-            accessToken: api_key
-          }).addTo(map);
-        } else {
-          L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
-            maxZoom: 19,
-            attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
-          }).addTo(map);
-        }
-        let marker = L.marker([lat, lng]).addTo(map);
-        let url = lat + ',' + lng + '#map=' + zoom + '/' + lat + '/' + lng + '&layers=N';
-        marker.bindPopup(address + '<p><a href="https://www.openstreetmap.org/directions?engine=osrm_car&route=' + url + '">Routing via OpenStreetMap</a></p>');
+        window.location.hash = '';
       }
     }
-  }
+  });
+}
+
+// Debounce input to prevent spamming filter requests.
+function debounce(fn, threshold) {
+  let timeout;
+  threshold = threshold || 100;
+  return function debounced() {
+    clearTimeout(timeout);
+    let args = arguments;
+    let _this = this;
+
+    function delayed() {
+      fn.apply(_this, args);
+    }
 
-  /* ---------------------------------------------------------------------------
-   * GitHub API.
-   * --------------------------------------------------------------------------- */
+    timeout = setTimeout(delayed, threshold);
+  };
+}
 
-  function printLatestRelease(selector, repo) {
-    $.getJSON('https://api.github.com/repos/' + repo + '/tags').done(function (json) {
-      let release = json[0];
-      $(selector).append(' ' + release.name);
-    }).fail(function (jqxhr, textStatus, error) {
-      let err = textStatus + ", " + error;
-      console.log("Request Failed: " + err);
-    });
+// Flatten object by concatenating values.
+function concatValues(obj) {
+  let value = '';
+  for (let prop in obj) {
+    value += obj[prop];
   }
+  return value;
+}
 
-  /* ---------------------------------------------------------------------------
-  * Toggle search dialog.
-  * --------------------------------------------------------------------------- */
+// Filter publications according to hash in URL.
+function filter_publications() {
+  // Check for Isotope publication layout.
+  if (!$grid_pubs.length)
+    return
 
-  function toggleSearchDialog() {
-    if ($('body').hasClass('searching')) {
-      // Clear search query and hide search modal.
-      $('[id=search-query]').blur();
-      $('body').removeClass('searching compensate-for-scrollbar');
+  let urlHash = window.location.hash.replace('#', '');
+  let filterValue = '*';
 
-      // Remove search query params from URL as user has finished searching.
-      removeQueryParamsFromUrl();
+  // Check if hash is numeric.
+  if (urlHash != '' && !isNaN(urlHash)) {
+    filterValue = '.pubtype-' + urlHash;
+  }
+
+  // Set filter.
+  let filterGroup = 'pubtype';
+  pubFilters[filterGroup] = filterValue;
+  filterValues = concatValues(pubFilters);
+
+  // Activate filters.
+  $grid_pubs.isotope();
+
+  // Set selected option.
+  $('.pubtype-select').val(filterValue);
+}
+
+/* ---------------------------------------------------------------------------
+* Google Maps or OpenStreetMap via Leaflet.
+* --------------------------------------------------------------------------- */
+
+function initMap() {
+  if ($('#map').length) {
+    let map_provider = $('#map-provider').val();
+    let lat = $('#map-lat').val();
+    let lng = $('#map-lng').val();
+    let zoom = parseInt($('#map-zoom').val());
+    let address = $('#map-dir').val();
+    let api_key = $('#map-api-key').val();
+
+    if (map_provider == 1) {
+      let map = new GMaps({
+        div: '#map',
+        lat: lat,
+        lng: lng,
+        zoom: zoom,
+        zoomControl: true,
+        zoomControlOpt: {
+          style: 'SMALL',
+          position: 'TOP_LEFT'
+        },
+        panControl: false,
+        streetViewControl: false,
+        mapTypeControl: false,
+        overviewMapControl: false,
+        scrollwheel: true,
+        draggable: true
+      });
 
-      // Prevent fixed positioned elements (e.g. navbar) moving due to scrollbars.
-      $('#fancybox-style-noscroll').remove();
+      map.addMarker({
+        lat: lat,
+        lng: lng,
+        click: function (e) {
+          let url = 'https://www.google.com/maps/place/' + encodeURIComponent(address) + '/@' + lat + ',' + lng + '/';
+          window.open(url, '_blank')
+        },
+        title: address
+      })
     } else {
-      // Prevent fixed positioned elements (e.g. navbar) moving due to scrollbars.
-      if (!$('#fancybox-style-noscroll').length && document.body.scrollHeight > window.innerHeight) {
-        $('head').append(
-          '<style id="fancybox-style-noscroll">.compensate-for-scrollbar{margin-right:' +
-          (window.innerWidth - document.documentElement.clientWidth) +
-          'px;}</style>'
-        );
-        $('body').addClass('compensate-for-scrollbar');
+      let map = new L.map('map').setView([lat, lng], zoom);
+      if (map_provider == 3 && api_key.length) {
+        L.tileLayer('https://api.mapbox.com/styles/v1/{id}/tiles/{z}/{x}/{y}?access_token={accessToken}', {
+          attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>, Imagery © <a href="http://mapbox.com">Mapbox</a>',
+          tileSize: 512,
+          maxZoom: 18,
+          zoomOffset: -1,
+          id: 'mapbox/streets-v11',
+          accessToken: api_key
+        }).addTo(map);
+      } else {
+        L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+          maxZoom: 19,
+          attribution: '&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
+        }).addTo(map);
       }
-
-      // Show search modal.
-      $('body').addClass('searching');
-      $('.search-results').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 200);
-      $('#search-query').focus();
+      let marker = L.marker([lat, lng]).addTo(map);
+      let url = lat + ',' + lng + '#map=' + zoom + '/' + lat + '/' + lng + '&layers=N';
+      marker.bindPopup(address + '<p><a href="https://www.openstreetmap.org/directions?engine=osrm_car&route=' + url + '">Routing via OpenStreetMap</a></p>');
     }
   }
+}
 
-  /* ---------------------------------------------------------------------------
-  * Change Theme Mode (0: Day, 1: Night, 2: Auto).
-  * --------------------------------------------------------------------------- */
-
-  // TODO: import theme functions from load-theme.js to avoid duplication.
-  function canChangeTheme() {
-    // If var is set, then user is allowed to change the theme variation.
-    return Boolean(window.wcDarkLightEnabled);
-  }
-
-  function getThemeMode() {
-    return parseInt(localStorage.getItem('dark_mode') || 2);
-  }
+/* ---------------------------------------------------------------------------
+ * GitHub API.
+ * --------------------------------------------------------------------------- */
 
-  function changeThemeModeClick(newMode) {
-    console.info('Request to set theme.');
-    if (!canChangeTheme()) {
-      console.info('Cannot set theme - admin disabled theme selector.');
-      return;
-    }
-    let isDarkTheme;
-    switch (newMode) {
-      case 0:
-        localStorage.setItem('dark_mode', '1');
-        isDarkTheme = true;
-        console.info('User changed theme variation to Dark.');
-        showActiveTheme(0);
-        break;
-      case 1:
-        localStorage.setItem('dark_mode', '2');
-        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
-          // The visitor prefers dark themes and switching to the dark variation is allowed by admin.
-          isDarkTheme = true;
-        } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
-          // The visitor prefers light themes and switching to the dark variation is allowed by admin.
-          isDarkTheme = false;
-        } else {
-          isDarkTheme = isSiteThemeDark;  // Use the site's default theme variation based on `light` in the theme file.
-        }
-        console.info('User changed theme variation to Auto.');
-        showActiveTheme(1);
-        break;
-      default:
-        localStorage.setItem('dark_mode', '0');
-        isDarkTheme = false;
-        console.info('User changed theme variation to Light.');
-        showActiveTheme(2);
-        break;
-    }
-    renderThemeVariation(isDarkTheme);
+function printLatestRelease(selector, repo) {
+  if (hugoEnvironment === 'production') {
+    $.getJSON('https://api.github.com/repos/' + repo + '/tags').done(function (json) {
+      let release = json[0];
+      $(selector).append(' ' + release.name);
+    }).fail(function (jqxhr, textStatus, error) {
+      let err = textStatus + ", " + error;
+      console.log("Request Failed: " + err);
+    });
   }
-
-  function showActiveTheme(mode){
-    switch (mode) {
-      case 0:
-        // Dark.
-        $('.js-set-theme-light').removeClass('dropdown-item-active');
-        $('.js-set-theme-dark').addClass('dropdown-item-active');
-        $('.js-set-theme-auto').removeClass('dropdown-item-active');
-        break;
-      case 1:
-        // Auto.
-        $('.js-set-theme-light').removeClass('dropdown-item-active');
-        $('.js-set-theme-dark').removeClass('dropdown-item-active');
-        $('.js-set-theme-auto').addClass('dropdown-item-active');
-        break;
-      default:
-        // Light.
-        $('.js-set-theme-light').addClass('dropdown-item-active');
-        $('.js-set-theme-dark').removeClass('dropdown-item-active');
-        $('.js-set-theme-auto').removeClass('dropdown-item-active');
-        break;
+}
+
+/* ---------------------------------------------------------------------------
+* Toggle search dialog.
+* --------------------------------------------------------------------------- */
+
+function toggleSearchDialog() {
+  if ($('body').hasClass('searching')) {
+    // Clear search query and hide search modal.
+    $('[id=search-query]').blur();
+    $('body').removeClass('searching compensate-for-scrollbar');
+
+    // Remove search query params from URL as user has finished searching.
+    removeQueryParamsFromUrl();
+
+    // Prevent fixed positioned elements (e.g. navbar) moving due to scrollbars.
+    $('#fancybox-style-noscroll').remove();
+  } else {
+    // Prevent fixed positioned elements (e.g. navbar) moving due to scrollbars.
+    if (!$('#fancybox-style-noscroll').length && document.body.scrollHeight > window.innerHeight) {
+      $('head').append(
+        '<style id="fancybox-style-noscroll">.compensate-for-scrollbar{margin-right:' +
+        (window.innerWidth - document.documentElement.clientWidth) +
+        'px;}</style>'
+      );
+      $('body').addClass('compensate-for-scrollbar');
     }
-  }
 
-  function getThemeVariation() {
-    if (!canChangeTheme()) {
-      return isSiteThemeDark;  // Use the site's default theme variation based on `light` in the theme file.
-    }
-    let currentThemeMode = getThemeMode();
-    let isDarkTheme;
-    switch (currentThemeMode) {
-      case 0:
-        isDarkTheme = false;
-        break;
-      case 1:
-        isDarkTheme = true;
-        break;
-      default:
-        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
-          // The visitor prefers dark themes and switching to the dark variation is allowed by admin.
-          isDarkTheme = true;
-        } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
-          // The visitor prefers light themes and switching to the dark variation is allowed by admin.
-          isDarkTheme = false;
-        } else {
-          isDarkTheme = isSiteThemeDark;  // Use the site's default theme variation based on `light` in the theme file.
-        }
-        break;
-    }
-    return isDarkTheme;
+    // Show search modal.
+    $('body').addClass('searching');
+    $('.search-results').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 200);
+    $('#search-query').focus();
   }
+}
+
+/* ---------------------------------------------------------------------------
+* Normalize Bootstrap Carousel Slide Heights.
+* --------------------------------------------------------------------------- */
+
+function normalizeCarouselSlideHeights() {
+  $('.carousel').each(function () {
+    // Get carousel slides.
+    let items = $('.carousel-item', this);
+    // Reset all slide heights.
+    items.css('min-height', 0);
+    // Normalize all slide heights.
+    let maxHeight = Math.max.apply(null, items.map(function () {
+      return $(this).outerHeight()
+    }).get());
+    items.css('min-height', maxHeight + 'px');
+  })
+}
+
+/* ---------------------------------------------------------------------------
+* Fix Hugo's Goldmark output and Mermaid code blocks.
+* --------------------------------------------------------------------------- */
+
+/**
+ * Fix Hugo's Goldmark output.
+ */
+function fixHugoOutput() {
+  // Fix Goldmark table of contents.
+  // - Must be performed prior to initializing ScrollSpy.
+  $('#TableOfContents').addClass('nav flex-column');
+  $('#TableOfContents li').addClass('nav-item');
+  $('#TableOfContents li a').addClass('nav-link');
+
+  // Fix Goldmark task lists (remove bullet points).
+  $("input[type='checkbox'][disabled]").parents('ul').addClass('task-list');
+}
+
+// Get an element's siblings.
+function getSiblings(elem) {
+  // Filter out itself.
+  return Array.prototype.filter.call(elem.parentNode.children, function (sibling) {
+    return sibling !== elem;
+  });
+}
 
-  /**
-   * Render theme variation (day or night).
-   *
-   * @param {boolean} isDarkTheme
-   * @param {boolean} init
-   * @returns {undefined}
-   */
-  function renderThemeVariation(isDarkTheme, init = false) {
-    // Is code highlighting enabled in site config?
-    const codeHlEnabled = $('link[title=hl-light]').length > 0;
-    const codeHlLight = $('link[title=hl-light]')[0];
-    const codeHlDark = $('link[title=hl-dark]')[0];
-    const diagramEnabled = $('script[title=mermaid]').length > 0;
-
-    // Check if re-render required.
-    if (!init) {
-      // If request to render light when light variation already rendered, return.
-      // If request to render dark when dark variation already rendered, return.
-      if ((isDarkTheme === false && !$('body').hasClass('dark')) || (isDarkTheme === true && $('body').hasClass('dark'))) {
-        return;
-      }
-    }
+/* ---------------------------------------------------------------------------
+ * On document ready.
+ * --------------------------------------------------------------------------- */
 
-    if (isDarkTheme === false) {
-      if (!init) {
-        // Only fade in the page when changing the theme variation.
-        $('body').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 500);
-      }
-      $('body').removeClass('dark');
-      if (codeHlEnabled) {
-        codeHlLight.disabled = false;
-        codeHlDark.disabled = true;
-      }
-      if (diagramEnabled) {
-        if (init) {
-          mermaid.initialize({theme: 'default', securityLevel: 'loose'});
-        } else {
-          // Have to reload to re-initialise Mermaid with the new theme and re-parse the Mermaid code blocks.
-          location.reload();
-        }
-      }
-    } else if (isDarkTheme === true) {
-      if (!init) {
-        // Only fade in the page when changing the theme variation.
-        $('body').css({opacity: 0, visibility: 'visible'}).animate({opacity: 1}, 500);
-      }
-      $('body').addClass('dark');
-      if (codeHlEnabled) {
-        codeHlLight.disabled = true;
-        codeHlDark.disabled = false;
-      }
-      if (diagramEnabled) {
-        if (init) {
-          mermaid.initialize({theme: 'dark', securityLevel: 'loose'});
-        } else {
-          // Have to reload to re-initialise Mermaid with the new theme and re-parse the Mermaid code blocks.
-          location.reload();
-        }
-      }
-    }
-  }
+$(document).ready(function () {
+  fixHugoOutput();
+  fixMermaid();
 
-  function initThemeVariation() {
-    // If theme changer component present, set its icon according to the theme mode (day, night, or auto).
-    if (canChangeTheme) {
-      let themeMode = getThemeMode();
-      switch (themeMode) {
-        case 0:
-          showActiveTheme(2);
-          console.info('Initialize theme variation to Light.');
-          break;
-        case 1:
-          showActiveTheme(0);
-          console.info('Initialize theme variation to Dark.');
-          break;
-        default:
-          showActiveTheme(1);
-          console.info('Initialize theme variation to Auto.');
-          break;
-      }
-    }
-    // Render the day or night theme.
-    let isDarkTheme = getThemeVariation();
-    renderThemeVariation(isDarkTheme, true);
+  // Initialise code highlighting if enabled for this page.
+  // Note: this block should be processed after the Mermaid code-->div conversion.
+  if (code_highlighting) {
+    hljs.initHighlighting();
   }
 
-  /* ---------------------------------------------------------------------------
-  * Normalize Bootstrap Carousel Slide Heights.
-  * --------------------------------------------------------------------------- */
-
-  function normalizeCarouselSlideHeights() {
-    $('.carousel').each(function () {
-      // Get carousel slides.
-      let items = $('.carousel-item', this);
-      // Reset all slide heights.
-      items.css('min-height', 0);
-      // Normalize all slide heights.
-      let maxHeight = Math.max.apply(null, items.map(function () {
-        return $(this).outerHeight()
-      }).get());
-      items.css('min-height', maxHeight + 'px');
-    })
-  }
+  // Render theme variation, including any HLJS and Mermaid themes.
+  let {isDarkTheme, themeMode} = initThemeVariation();
+  renderThemeVariation(isDarkTheme, themeMode, true);
+});
 
-  /* ---------------------------------------------------------------------------
- * Fix Hugo's Goldmark output and Mermaid code blocks.
+/* ---------------------------------------------------------------------------
+ * On window loaded.
  * --------------------------------------------------------------------------- */
 
-  /**
-   * Fix Hugo's Goldmark output.
-   */
-  function fixHugoOutput() {
-    // Fix Goldmark table of contents.
-    // - Must be performed prior to initializing ScrollSpy.
-    $('#TableOfContents').addClass('nav flex-column');
-    $('#TableOfContents li').addClass('nav-item');
-    $('#TableOfContents li a').addClass('nav-link');
-
-    // Fix Goldmark task lists (remove bullet points).
-    $("input[type='checkbox'][disabled]").parents('ul').addClass('task-list');
-  }
-
-  /**
-   * Fix Mermaid.js clash with Highlight.js.
-   * Refactor Mermaid code blocks as divs to prevent Highlight parsing them and enable Mermaid to parse them.
-   */
-  function fixMermaid() {
-    let mermaids = [];
-    [].push.apply(mermaids, document.getElementsByClassName('language-mermaid'));
-    for (let i = 0; i < mermaids.length; i++) {
-      $(mermaids[i]).unwrap('pre');  // Remove <pre> wrapper.
-      $(mermaids[i]).replaceWith(function () {
-        // Convert <code> block to <div> and add `mermaid` class so that Mermaid will parse it.
-        return $("<div />").append($(this).contents()).addClass('mermaid');
-      });
+$(window).on('load', function () {
+  // Init Isotope Layout Engine for instances of the Portfolio widget.
+  let isotopeInstances = document.querySelectorAll('.projects-container');
+  let isotopeInstancesCount = isotopeInstances.length;
+  let isotopeCounter = 0;
+  isotopeInstances.forEach(function (isotopeInstance, index) {
+    console.debug(`Loading Isotope instance ${index}`);
+
+    // Isotope instance
+    let iso;
+
+    // Get the layout for this Isotope instance
+    let isoSection = isotopeInstance.closest('section');
+    let layout = '';
+    if (isoSection.querySelector('.isotope').classList.contains('js-layout-row')) {
+      layout = 'fitRows';
+    } else {
+      layout = 'masonry';
     }
-  }
-
-  /* ---------------------------------------------------------------------------
-   * On document ready.
-   * --------------------------------------------------------------------------- */
-
-  $(document).ready(function () {
-    fixHugoOutput();
-    fixMermaid();
 
-    // Initialise code highlighting if enabled for this page.
-    // Note: this block should be processed after the Mermaid code-->div conversion.
-    if (code_highlighting) {
-      hljs.initHighlighting();
+    // Get default filter (if any) for this instance
+    let defaultFilter = isoSection.querySelector('.default-project-filter');
+    let filterText = '*';
+    if (defaultFilter !== null) {
+      filterText = defaultFilter.textContent;
     }
+    console.debug(`Default Isotope filter: ${filterText}`);
+
+    // Init Isotope instance once its images have loaded.
+    imagesLoaded(isotopeInstance, function () {
+      iso = new Isotope(isotopeInstance, {
+        itemSelector: '.isotope-item',
+        layoutMode: layout,
+        masonry: {
+          gutter: 20
+        },
+        filter: filterText
+      });
 
-    // Initialize theme variation.
-    initThemeVariation();
-
-    // Change theme mode.
-    $('.js-set-theme-light').click(function (e) {
-      e.preventDefault();
-      changeThemeModeClick(2);
-    });
-    $('.js-set-theme-dark').click(function (e) {
-      e.preventDefault();
-      changeThemeModeClick(0);
-    });
-    $('.js-set-theme-auto').click(function (e) {
-      e.preventDefault();
-      changeThemeModeClick(1);
-    });
+      // Filter Isotope items when a toolbar filter button is clicked.
+      let isoFilterButtons = isoSection.querySelectorAll('.project-filters a');
+      isoFilterButtons.forEach(button => button.addEventListener('click', (e) => {
+        e.preventDefault();
+        let selector = button.getAttribute('data-filter');
+
+        // Apply filter
+        console.debug(`Updating Isotope filter to ${selector}`);
+        iso.arrange({filter: selector});
+
+        // Update active toolbar filter button
+        button.classList.remove('active');
+        button.classList.add('active');
+        let buttonSiblings = getSiblings(button);
+        buttonSiblings.forEach(buttonSibling => {
+          buttonSibling.classList.remove('active');
+          buttonSibling.classList.remove('all');
+        });
+      }));
 
-    // Live update of day/night mode on system preferences update (no refresh required).
-    // Note: since we listen only for *dark* events, we won't detect other scheme changes such as light to no-preference.
-    const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
-    darkModeMediaQuery.addListener((e) => {
-      if (!canChangeTheme()) {
-        // Changing theme variation is not allowed by admin.
-        return;
-      }
-      const darkModeOn = e.matches;
-      console.log(`OS dark mode preference changed to ${darkModeOn ? '🌒 on' : '☀️ off'}.`);
-      let currentThemeVariation = parseInt(localStorage.getItem('dark_mode') || 2);
-      let isDarkTheme;
-      if (currentThemeVariation === 2) {
-        if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
-          // The visitor prefers dark themes.
-          isDarkTheme = true;
-        } else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
-          // The visitor prefers light themes.
-          isDarkTheme = false;
-        } else {
-          // The visitor does not have a day or night preference, so use the theme's default setting.
-          isDarkTheme = isSiteThemeDark;
-        }
-        renderThemeVariation(isDarkTheme);
-      }
+      // Check if all Isotope instances have loaded.
+      incrementIsotopeCounter();
     });
   });
 
-  /* ---------------------------------------------------------------------------
-   * On window loaded.
-   * --------------------------------------------------------------------------- */
-
-  $(window).on('load', function () {
-    // Filter projects.
-    $('.projects-container').each(function (index, container) {
-      let $container = $(container);
-      let $section = $container.closest('section');
-      let layout;
-      if ($section.find('.isotope').hasClass('js-layout-row')) {
-        layout = 'fitRows';
-      } else {
-        layout = 'masonry';
+  // Hook to perform actions once all Isotope instances have loaded.
+  function incrementIsotopeCounter() {
+    isotopeCounter++;
+    if (isotopeCounter === isotopeInstancesCount) {
+      console.debug(`All Portfolio Isotope instances loaded.`);
+      // Once all Isotope instances and their images have loaded, scroll to hash (if set).
+      // Prevents scrolling to the wrong location due to the dynamic height of Isotope instances.
+      // Each Isotope instance height is affected by applying filters and loading images.
+      // Without this logic, the scroll location can appear correct, but actually a few pixels out and hence Scrollspy
+      // can highlight the wrong nav link.
+      if (window.location.hash) {
+        scrollToAnchor(decodeURIComponent(window.location.hash), 0);
       }
+    }
+  }
 
-      $container.imagesLoaded(function () {
-        // Initialize Isotope after all images have loaded.
-        $container.isotope({
-          itemSelector: '.isotope-item',
-          layoutMode: layout,
-          masonry: {
-            gutter: 20
-          },
-          filter: $section.find('.default-project-filter').text()
-        });
-
-        // Filter items when filter link is clicked.
-        $section.find('.project-filters a').click(function () {
-          let selector = $(this).attr('data-filter');
-          $container.isotope({filter: selector});
-          $(this).removeClass('active').addClass('active').siblings().removeClass('active all');
-          return false;
-        });
+  // Enable publication filter for publication index page.
+  if ($('.pub-filters-select')) {
+    filter_publications();
+    // Useful for changing hash manually (e.g. in development):
+    // window.addEventListener('hashchange', filter_publications, false);
+  }
 
-        // If window hash is set, scroll to hash.
-        // Placing this within `imagesLoaded` prevents scrolling to the wrong location due to dynamic image loading
-        // affecting page layout and position of the target anchor ID.
-        // Note: If there are multiple project widgets on a page, ideally only perform this once after images
-        // from *all* project widgets have finished loading.
-        if (window.location.hash) {
-          scrollToAnchor();
-        }
-      });
+  // Load citation modal on 'Cite' click.
+  $('.js-cite-modal').click(function (e) {
+    e.preventDefault();
+    let filename = $(this).attr('data-filename');
+    let modal = $('#modal');
+    modal.find('.modal-body code').load(filename, function (response, status, xhr) {
+      if (status == 'error') {
+        let msg = "Error: ";
+        $('#modal-error').html(msg + xhr.status + " " + xhr.statusText);
+      } else {
+        $('.js-download-cite').attr('href', filename);
+      }
     });
+    modal.modal('show');
+  });
 
-    // Enable publication filter for publication index page.
-    if ($('.pub-filters-select')) {
-      filter_publications();
-      // Useful for changing hash manually (e.g. in development):
-      // window.addEventListener('hashchange', filter_publications, false);
+  // Copy citation text on 'Copy' click.
+  $('.js-copy-cite').click(function (e) {
+    e.preventDefault();
+    // Get selection.
+    let range = document.createRange();
+    let code_node = document.querySelector('#modal .modal-body');
+    range.selectNode(code_node);
+    window.getSelection().addRange(range);
+    try {
+      // Execute the copy command.
+      document.execCommand('copy');
+    } catch (e) {
+      console.log('Error: citation copy failed.');
     }
+    // Remove selection.
+    window.getSelection().removeRange(range);
+  });
 
-    // Scroll to top of page.
-    $('.back-to-top').click(function (event) {
-      event.preventDefault();
-      $('html, body').animate({
-        'scrollTop': 0
-      }, 800, function () {
-        window.location.hash = "";
-      });
-    });
+  // Initialise Google Maps if necessary.
+  initMap();
 
-    // Load citation modal on 'Cite' click.
-    $('.js-cite-modal').click(function (e) {
-      e.preventDefault();
-      let filename = $(this).attr('data-filename');
-      let modal = $('#modal');
-      modal.find('.modal-body code').load(filename, function (response, status, xhr) {
-        if (status == 'error') {
-          let msg = "Error: ";
-          $('#modal-error').html(msg + xhr.status + " " + xhr.statusText);
-        } else {
-          $('.js-download-cite').attr('href', filename);
-        }
-      });
-      modal.modal('show');
-    });
+  // Print latest version of GitHub projects.
+  let githubReleaseSelector = '.js-github-release';
+  if ($(githubReleaseSelector).length > 0) {
+    printLatestRelease(githubReleaseSelector, $(githubReleaseSelector).data('repo'));
+  }
 
-    // Copy citation text on 'Copy' click.
-    $('.js-copy-cite').click(function (e) {
-      e.preventDefault();
-      // Get selection.
-      let range = document.createRange();
-      let code_node = document.querySelector('#modal .modal-body');
-      range.selectNode(code_node);
-      window.getSelection().addRange(range);
-      try {
-        // Execute the copy command.
-        document.execCommand('copy');
-      } catch (e) {
-        console.log('Error: citation copy failed.');
+  // Parse Wowchemy keyboard shortcuts.
+  document.addEventListener('keyup', (event) => {
+    if (event.code === "Escape") {
+      const body = document.body;
+      if (body.classList.contains('searching')) {
+        // Close search dialog.
+        toggleSearchDialog();
       }
-      // Remove selection.
-      window.getSelection().removeRange(range);
-    });
-
-    // Initialise Google Maps if necessary.
-    initMap();
-
-    // Print latest version of GitHub projects.
-    let githubReleaseSelector = '.js-github-release';
-    if ($(githubReleaseSelector).length > 0)
-      printLatestRelease(githubReleaseSelector, $(githubReleaseSelector).data('repo'));
+    }
+    // Use `key` to check for slash. Otherwise, with `code` we need to check for modifiers.
+    if (event.key === "/" ) {
+      let focusedElement = (
+        document.hasFocus() &&
+        document.activeElement !== document.body &&
+        document.activeElement !== document.documentElement &&
+        document.activeElement
+      ) || null;
+      let isInputFocused = focusedElement instanceof HTMLInputElement || focusedElement instanceof HTMLTextAreaElement;
+      if (search_config && !isInputFocused) {
+        // Open search dialog.
+        event.preventDefault();
+        toggleSearchDialog();
+      }
+    }
+  });
 
+  // Search event handler
+  // Check that built-in search or Algolia enabled.
+  if (search_config) {
     // On search icon click toggle search dialog.
     $('.js-search').click(function (e) {
       e.preventDefault();
       toggleSearchDialog();
     });
-    $(document).on('keydown', function (e) {
-      if (e.which == 27) {
-        // `Esc` key pressed.
-        if ($('body').hasClass('searching')) {
-          toggleSearchDialog();
-        }
-      } else if (e.which == 191 && e.shiftKey == false && !$('input,textarea').is(':focus')) {
-        // `/` key pressed outside of text input.
-        e.preventDefault();
-        toggleSearchDialog();
-      }
-    });
-
-    // Init. author notes (tooltips).
-    $('[data-toggle="tooltip"]').tooltip();
+  }
 
+  // Init. author notes (tooltips).
+  $('[data-toggle="tooltip"]').tooltip();
+
+  // Re-initialize Scrollspy with dynamic navbar height offset.
+  fixScrollspy();
+});
+
+// Theme chooser events.
+let linkLight = document.querySelector('.js-set-theme-light');
+let linkDark = document.querySelector('.js-set-theme-dark');
+let linkAuto = document.querySelector('.js-set-theme-auto');
+if (linkLight && linkDark && linkAuto) {
+  linkLight.addEventListener('click', event => {
+    event.preventDefault();
+    changeThemeModeClick(0);
   });
-
-  // Normalize Bootstrap carousel slide heights.
-  $(window).on('load resize orientationchange', normalizeCarouselSlideHeights);
-
-  // Automatic main menu dropdowns on mouse over.
-  $('body').on('mouseenter mouseleave', '.dropdown', function (e) {
-    var dropdown = $(e.target).closest('.dropdown');
-    var menu = $('.dropdown-menu', dropdown);
-    dropdown.addClass('show');
-    menu.addClass('show');
-    setTimeout(function () {
-      dropdown[dropdown.is(':hover') ? 'addClass' : 'removeClass']('show');
-      menu[dropdown.is(':hover') ? 'addClass' : 'removeClass']('show');
-    }, 300);
-
-    // Re-initialize Scrollspy with dynamic navbar height offset.
-    fixScrollspy();
-
-    if (window.location.hash) {
-      // When accessing homepage from another page and `#top` hash is set, show top of page (no hash).
-      if (window.location.hash == "#top") {
-        window.location.hash = ""
-      } else if (!$('.projects-container').length) {
-        // If URL contains a hash and there are no dynamically loaded images on the page,
-        // immediately scroll to target ID taking into account responsive offset.
-        // Otherwise, wait for `imagesLoaded()` to complete before scrolling to hash to prevent scrolling to wrong
-        // location.
-        scrollToAnchor();
-      }
-    }
-
-    // Call `fixScrollspy` when window is resized.
-    let resizeTimer;
-    $(window).resize(function () {
-      clearTimeout(resizeTimer);
-      resizeTimer = setTimeout(fixScrollspy, 200);
-    });
+  linkDark.addEventListener('click', event => {
+    event.preventDefault();
+    changeThemeModeClick(1);
   });
-
-})(jQuery);
+  linkAuto.addEventListener('click', event => {
+    event.preventDefault();
+    changeThemeModeClick(2);
+  });
+}
+
+// Media Query events.
+// Live update of day/night mode on system preferences update (no refresh required).
+// Note: since we listen only for *dark* events, we won't detect other scheme changes such as light to no-preference.
+const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
+darkModeMediaQuery.addEventListener("change", (event) => {
+  onMediaQueryListEvent(event);
+});
+
+// Normalize Bootstrap carousel slide heights for Slider widget instances.
+window.addEventListener('load', normalizeCarouselSlideHeights);
+window.addEventListener('resize', normalizeCarouselSlideHeights);
+window.addEventListener('orientationchange', normalizeCarouselSlideHeights);
+
+// Automatic main menu dropdowns on mouse over.
+$('body').on('mouseenter mouseleave', '.dropdown', function (e) {
+  var dropdown = $(e.target).closest('.dropdown');
+  var menu = $('.dropdown-menu', dropdown);
+  dropdown.addClass('show');
+  menu.addClass('show');
+  setTimeout(function () {
+    dropdown[dropdown.is(':hover') ? 'addClass' : 'removeClass']('show');
+    menu[dropdown.is(':hover') ? 'addClass' : 'removeClass']('show');
+  }, 300);
+});
+
+// Call `fixScrollspy` when window is resized.
+let resizeTimer;
+$(window).resize(function () {
+  clearTimeout(resizeTimer);
+  resizeTimer = setTimeout(fixScrollspy, 200);
+});

+ 1 - 0
wowchemy/assets/scss/main.scss

@@ -20,6 +20,7 @@ $sta-primary-dark: darken($sta-primary, $sta-darken-percentage);
 
 $sta-link: {{ $scr.Get "link" }};
 $sta-link-hover: {{ $scr.Get "link_hover" }};
+$sta-link-decoration: {{ $scr.Get "link_decoration" }};
 
 $sta-dark-link: {{ $scr.Get "dark_link" }};
 $sta-dark-link-hover: {{ $scr.Get "dark_link_hover" }};

+ 15 - 0
wowchemy/assets/scss/wowchemy/_breadcrumb.scss

@@ -0,0 +1,15 @@
+.breadcrumb {
+  font-size: 14px;
+  padding: 0rem;
+  background-color: transparent;
+  border-radius: 0rem;
+  margin-bottom: 0rem;
+}
+
+.breadcrumb-item.active {
+  color: rgba(0, 0, 0, 0.54);
+}
+
+.dark .breadcrumb-item.active {
+  color: rgba(255, 255, 255, 0.54);
+}

+ 66 - 0
wowchemy/assets/scss/wowchemy/_callouts.scss

@@ -0,0 +1,66 @@
+// Callout styles for the Callout shortcode
+
+/* Style asides as Bootstrap alerts. */
+.article-style aside {
+  @extend .alert;
+}
+
+/* Asides use <p> block element whereas alerts use <div>. */
+.article-style aside p,
+div.alert > div {
+  position: relative;
+  display: block;
+  font-size: 1rem;
+  margin-left: 2rem;
+  margin-top: 0;
+  margin-bottom: 0;
+}
+
+div.alert div > * {
+  margin-bottom: .5rem;  /* Use smaller paragraph spacing than usual. */
+}
+
+div.alert div > :last-child {
+  margin-bottom: 0;
+}
+
+.article-style aside p::before,
+div.alert > div:first-child::before {
+  position: absolute;
+  top: -0.5rem;
+  left: -2rem;
+  font-size: 1.5rem;
+  color: #209cee;
+  font-family: 'Font Awesome 5 Free';
+  font-weight: 900;
+  content: '\f05a';
+  width: 1.5rem;
+  text-align: center;
+}
+
+div.alert-warning > div:first-child::before {
+  font-family: 'Font Awesome 5 Free';
+  font-weight: 900;
+  color: #ff3860;
+  content: '\f071';
+}
+
+.article-style aside a,
+div.alert a {
+  color: currentColor;
+  text-decoration: none;
+  border-bottom: solid 1px currentColor;
+}
+
+.article-style aside,
+.alert-note {
+  color: #12537e;
+  background-color: #f6fbfe;
+  border-color: #209cee;
+}
+
+.alert-warning {
+  color: #cd0930;
+  background-color: #fff5f7;
+  border-color: #ff3860;
+}

+ 15 - 8
wowchemy/assets/scss/wowchemy/_content.scss

@@ -139,25 +139,32 @@ article .article-metadata {
   // Break very long words such as pasted URL strings.
   overflow-wrap: break-word;
   word-wrap: break-word;
-  -ms-word-break: break-all;
-  word-break: break-all;
+  //-ms-word-break: break-all;
+  //word-break: break-all;
   word-break: break-word;
 
   // Add a hyphen where the word breaks, if supported (No Blink).
-  -ms-hyphens: auto;
-  -moz-hyphens: auto;
-  -webkit-hyphens: auto;
-  hyphens: auto;
+  //-ms-hyphens: auto;
+  //-moz-hyphens: auto;
+  //-webkit-hyphens: auto;
+  //hyphens: auto;
 }
 
 .article-style {
-  // Break unresponsive block content, such as unresponsive embeds, to prevent horizontal scrolling.
-  overflow-x: hidden;
+  // Any unresponsive embeds, e.g. Plotly, should be wrapped to handle on a case-by-case basis.
+  // If any unresponsive embeds still remain in the article, prevent article overflow and DIV scrolling.
+  // Note that lazy-loaded images, e.g. Gallery, may cause scrollable Y-overflow if only X-overflow is hidden pre-load.
+  overflow: hidden;
 
   // Word wrap text content.
   @include word-wrap();
 }
 
+// Underline links if they are a similar color to the body text.
+.article-style a {
+  text-decoration: $sta-link-decoration;
+}
+
 .article-style img,
 .article-style video {
   margin-left: auto;

+ 1 - 1
wowchemy/assets/scss/wowchemy/_dark.scss

@@ -29,7 +29,7 @@ body.dark,
 .dark h4,
 .dark h5,
 .dark h6 {
-  color: rgb(152, 166, 173);
+  color: #fff;
 }
 
 .dark pre,

+ 2 - 6
wowchemy/assets/scss/wowchemy/_footer.scss

@@ -2,7 +2,6 @@
 
 footer {
   margin: 4rem 0 0;
-  padding: 2rem 0;
   width: 100%;
 }
 
@@ -15,14 +14,12 @@ footer .powered-by {
   font-size: 0.67rem;
 }
 
-.site-footer,
-footer a.back-to-top i {
+.site-footer {
   color: rgba(0,0,0,0.54);
 }
 
 // Dark footer theme
 .dark .site-footer,
-.dark footer a.back-to-top i,
 .dark .docs .body-footer {
   color: rgba(255,255,255,0.54);
 }
@@ -37,11 +34,10 @@ footer a.back-to-top i {
   list-style: none;
   height: auto;
   width: auto;
-  font-size: 0;  // Hack to remove space characters between icons without using UL.
   text-decoration: none;
 }
 
-.footer-license-icons img {
+.footer-license-icons i {
  display: inline-flex;
   margin-right: 8px;
   height: 22px;

+ 11 - 1
wowchemy/assets/scss/wowchemy/_integrations.scss

@@ -1,6 +1,16 @@
-/* Mermaid.js div */
+// Mermaid.js diagram div
 div.mermaid {
   width: 100%;
   text-align: center;
   margin-bottom: 1rem;
 }
+
+// Plotly chart
+div.chart {
+  max-width: 100%;
+  margin-left: auto;
+  margin-right: auto;
+  margin-bottom: 1rem;
+  // Add horizontal scroll on mobile since Plotly
+  overflow-x: auto;
+}

+ 70 - 77
wowchemy/assets/scss/wowchemy/_root.scss

@@ -1,5 +1,5 @@
 /*************************************************
- *  Academic's Core
+ *  Wowchemy's Core Style
  **************************************************/
 
 html {
@@ -7,6 +7,14 @@ html {
   font-size: #{$sta-font-size-small}px;
   color: rgba(0,0,0,0.8);
   line-height: 1.65;
+
+  // Offset anchor scrolling by height of desktop fixed header.
+  scroll-padding-top: 70px;
+
+  @include media-breakpoint-down(md) {
+    // Offset anchor scrolling by height of mobile fixed header.
+    scroll-padding-top: 50px;
+  }
 }
 @media screen and (min-width: 58em) {
   html {
@@ -20,19 +28,55 @@ body {
   line-height: inherit;
   color: inherit;
   background-color: $sta-background;
-  margin-top: 70px; /* Offset body content by navbar height. */
   padding-top: 0;
   counter-reset: captions;
-}
-@include media-breakpoint-down(md) { /* Match max-width of .nav-bar query. */
-  body {
-    margin-top: 50px; /* Offset body content by navbar height. */
+
+  // Prevent horizontal scrollbar in case a site admin adds fixed width content without applying `max-width: 100%`.
+  overflow-x: hidden;
+
+  // Offset body content by fixed navbar height.
+  margin-top: 70px;
+
+  @include media-breakpoint-down(md) {
+    // Offset body content by fixed navbar height.
+    margin-top: 50px;
   }
 }
 body.no-navbar {
   margin-top: 0 !important;
+  scroll-padding-top: 0 !important;
+}
+
+// PAGE LAYOUT
+// Designed to push footer to bottom of viewport for short pages.
+
+.page-wrapper {
+  // Min height = viewport height - navbar height
+  min-height: calc(100vh - 70px);
+  display: grid;
+  grid-template-rows: auto 1fr auto;
+  grid-template-columns: 100%;
+}
+@include media-breakpoint-down(md) {
+  .page-wrapper {
+    min-height: calc(100vh - 50px);
+  }
+}
+.page-wrapper.no-navbar {
+  min-height: 100vh;
+}
+
+.page-header,
+.page-footer {
+  flex-shrink: 0;
 }
 
+.page-body {
+  flex-grow: 1;
+}
+
+// UTILITIES
+
 .max-width-640 {
   max-width: 640px;
 }
@@ -162,6 +206,21 @@ video {
   margin: 0 auto;
 }
 
+/* Hide overflowing of zoomed child img element */
+.img-hover-zoom {
+  overflow: hidden;
+}
+
+/* Smooth transition for image zoom on hover */
+.img-hover-zoom img {
+  transition: transform .3s ease-in-out;
+}
+
+/* Transform the image scale when container gets hovered */
+.img-hover-zoom:hover img {
+  transform: scale(1.1);
+}
+
 // Center all figure images by default.
 figure img {
   @extend .margin-auto;
@@ -478,10 +537,13 @@ a.badge:hover {
 table {
   display: block;
   width: 100%;
-  overflow-x: auto;
-  -webkit-overflow-scrolling: touch;
+  overflow-x: scroll;
   margin-bottom: 1rem;
   font-size: 0.8rem;
+  // Override `article-style`'s `break-word` wrapping to make wide tables scrollable.
+  overflow-wrap: normal;
+  word-wrap: normal;
+  word-break: normal;
 }
 
 table > thead > tr > th,
@@ -529,72 +591,3 @@ table > tbody > tr:hover > td,
 table > tbody > tr:hover > th {
   background-color: #e5e5e5;
 }
-
-/*************************************************
- *  Article Alerts (Shortcode) and Asides (Mmark)
- **************************************************/
-
-/* Style asides as Bootstrap alerts. */
-.article-style aside {
-  @extend .alert;
-}
-
-/* Asides use <p> block element whereas alerts use <div>. */
-.article-style aside p,
-div.alert > div {
-  position: relative;
-  display: block;
-  font-size: 1rem;
-  margin-left: 2rem;
-  margin-top: 0;
-  margin-bottom: 0;
-}
-
-div.alert div > * {
-  margin-bottom: .5rem;  /* Use smaller paragraph spacing than usual. */
-}
-
-div.alert div > :last-child {
-  margin-bottom: 0;
-}
-
-.article-style aside p::before,
-div.alert > div:first-child::before {
-  position: absolute;
-  top: -0.5rem;
-  left: -2rem;
-  font-size: 1.5rem;
-  color: #209cee;
-  font-family: 'Font Awesome 5 Free';
-  font-weight: 900;
-  content: '\f05a';
-  width: 1.5rem;
-  text-align: center;
-}
-
-div.alert-warning > div:first-child::before {
-  font-family: 'Font Awesome 5 Free';
-  font-weight: 900;
-  color: #ff3860;
-  content: '\f071';
-}
-
-.article-style aside a,
-div.alert a {
-  color: currentColor;
-  text-decoration: none;
-  border-bottom: solid 1px currentColor;
-}
-
-.article-style aside,
-.alert-note {
-  color: #12537e;
-  background-color: #f6fbfe;
-  border-color: #209cee;
-}
-
-.alert-warning {
-  color: #cd0930;
-  background-color: #fff5f7;
-  border-color: #ff3860;
-}

+ 24 - 1
wowchemy/assets/scss/wowchemy/_search.scss

@@ -66,7 +66,8 @@
   text-align: right;
 }
 
-.search-header i {
+// Large icon for closing search dialog.
+.search-header .col-search-close i {
   font-size: 2rem;
   line-height: 1;
 }
@@ -125,6 +126,28 @@
   box-shadow: 0 0 0 .2rem $sta-primary-light;
 }
 
+// Common queries
+
+#search-common-queries ul {
+  // Empirically remove indentation due to `fa-ul`'s centered 2em spacing, wider than the search icon.
+  margin-left: 0;
+  padding-left: 1.6em;
+}
+
+#search-common-queries li {
+  // Vertically align FA icons.
+  line-height: 1;
+}
+
+#search-common-queries li a {
+  // Color common search query links as body text.
+  color: inherit;
+}
+
+.dark #search-common-queries li a {
+  color: rgb(248, 248, 242);
+}
+
 /* DARK themed components. */
 
 /* Algolia search input */

+ 34 - 0
wowchemy/assets/scss/wowchemy/_shortcodes.scss

@@ -0,0 +1,34 @@
+// CTA Shortcode
+
+.cta-group {
+  --button-group-margin: 0.75em;
+  display: flex;
+  flex-wrap: wrap;
+  list-style: none;
+  align-items: center;
+  justify-content: left;
+  margin-left: calc(-1 * var(--button-group-margin));
+  margin-right: calc(-1 * var(--button-group-margin));
+  padding: 0;
+}
+
+.cta-group-center {
+  justify-content: center;
+}
+
+.cta-group li {
+  margin-bottom: 1em;
+  margin-left: var(--button-group-margin);
+  margin-right: var(--button-group-margin);
+}
+
+// Spoilers (toggle lists)
+
+details {
+  margin-bottom: 1rem;
+}
+
+summary:focus {
+  // Override Webkit setting an outline.
+  outline: none;
+}

+ 40 - 37
wowchemy/assets/scss/wowchemy/_widgets.scss

@@ -2,21 +2,12 @@
  *  Page Builder: sections and widgets
  **************************************************/
 
-@keyframes intro {
-  0% {
-    opacity: 0;
-  }
-  100% {
-    opacity: 1;
-  }
-}
-
 .home-section {
+  // Use `background` rather than `background-color` so it can support gradients in theme packs.
+  background: $sta-home-section-odd;
   position: relative;  // Required for component positioning within section.
-  background-color: $sta-home-section-odd;
   padding: 110px 0 110px 0;
-  animation: intro 0.3s both;
-  animation-delay: 0.15s;
+  z-index: 0;  // Explicit z-order otherwise `.home-section-bg` can be hidden by any `.home-section` background.
 }
 
 // Responsive fullscreen option for widgets
@@ -28,6 +19,9 @@
     min-height: calc(100vh - 50px);
   }
 }
+.no-navbar .home-section.fullscreen {
+  min-height: 100vh;
+}
 
 /* Override dark colors that may be inherited from body.dark */
 .home-section.dark,
@@ -35,7 +29,7 @@
 .home-section.dark h2,
 .home-section.dark h3,
 .home-section.dark a:not(.btn) {
-  color: rgb(248, 248, 242);
+  color: #fff;
 }
 
 /* Underline links in dark sections to separate them from text */
@@ -66,33 +60,48 @@
   top: 100%;
 }*/
 
+// Fill padding of `.home-section` parent
+.home-section-bg {
+  position: absolute;
+  top: 0;
+  left: 0;
+  height: 100%; // Or fill-available when supported.
+  width: 100%; // Or fill-available when supported.
+  z-index: -1; // Place bg div behind content.
+}
+
 /* Default background image properties for home sections. */
-.home-section.bg-image {
+.home-section-bg.bg-image {
   background-position: center;
   background-repeat: no-repeat;
   background-size: cover;
 }
 
-/* Create a parallax-like scrolling effect. */
+/* Create a parallax-like scrolling effect on desktop browsers. */
 .parallax {
-  height: 100%;
   background-attachment: fixed;
 }
+// Workaround issue with mobile browser support for fixed parallax background.
+@include media-breakpoint-down(md) {
+  .parallax {
+    background-attachment: scroll;
+  }
+}
 
 .home-section:first-of-type {
   padding-top: 50px;
 }
 
 .home-section:nth-of-type(even) {
-  background-color: $sta-home-section-even;
+  background: $sta-home-section-even;
 }
 
 .dark .home-section {
-  background-color: $sta-dark-home-section-odd;
+  background: $sta-dark-home-section-odd;
 }
 
 .dark .home-section:nth-of-type(even) {
-  background-color: $sta-dark-home-section-even;
+  background: $sta-dark-home-section-even;
 }
 
 @media screen and (max-width: 768px) {
@@ -136,14 +145,7 @@
  **************************************************/
 
 .wg-hero {
-  padding: 3em 0;
-  clear: both;
-  background-size: cover;
-  background-repeat: no-repeat;
-  background-position: center;
-  animation: intro 0.3s both;
-  animation-delay: 0s;
-  animation-delay: 0.25s;
+  padding: 3em 0; // More compact top and bottom padding for Hero.
 }
 
 .hero-title {
@@ -220,7 +222,7 @@ a.hero-cta-alt:hover {
  *  Slider Widget
  **************************************************/
 
-/* Clear `.home-section` as padding and animation interferes with Slider's layout and animations. */
+/* Clear `.home-section` as any padding or animation interferes with Slider's layout and animations. */
 .home-section.wg-slider {
   padding: 0;
   animation: none;
@@ -228,7 +230,7 @@ a.hero-cta-alt:hover {
 }
 
 /* The Slider widget reuses the Hero widget's `.wg-hero` class.
- * We must remove the `animation` and `clear` in this instance or
+ * We must remove any `animation` and `clear` (although Hero no longer sets `clear: both`) in this instance or
  * multiple slides can be `.active` at once. */
 .carousel-inner .wg-hero {
   animation: none;
@@ -401,7 +403,8 @@ ul.ul-edu li .description p.institution {
   color: rgb(248, 248, 242) !important;
 }
 
-.card .card-text ul {
+// For a UL after P, remove the spacing between (P margin-bottom) without affecting any nested lists.
+.card .card-text p + ul {
   margin-top: -1rem;
   margin-bottom: 0rem;
 }
@@ -458,11 +461,11 @@ ul.ul-edu li .description p.institution {
 
 .project-card {
   position: relative;
-  width: calc(33.3% - 2*20px); /* Fluid 3 columns (inc. 20px gutter) */
+  width: calc(33.3% - 13.3px); /* Fluid 3 columns ($gutter * ($number_of_cols - 1) / $number_of_cols; following https://stackoverflow.com/a/51290967) */
 }
 @media screen and (max-width: 1199px) {
   .project-card {
-    width: calc(50% - 20px); /* Fluid 2 columns (inc. 20px gutter) */
+    width: calc(50% - 10px); /* Fluid 2 columns ($gutter * ($number_of_cols - 1) / $number_of_cols; following https://stackoverflow.com/a/51290967) */
   }
 }
 @media screen and (max-width: 768px) {
@@ -558,24 +561,24 @@ ul.ul-edu li .description p.institution {
  *  Contact
  **************************************************/
 
-.contact-widget .fa-ul {
+.wg-contact .fa-ul {
   margin-left: 3.14285714rem; /* Must be > `fa-2x` icon size. */
 }
 
-.contact-widget .fa-li {
+.wg-contact .fa-li {
   position: absolute;
-  left: -3.14285714rem; /* Negative of `.contact-widget .fa-ul` margin. */
+  left: -3.14285714rem; /* Negative of `.wg-contact .fa-ul` margin. */
   width: 2rem; /* Match `fa-2x` icon size. */
   top: 0.14285714em; /* Default FA value. */
   text-align: center;
 }
 
-.contact-widget li {
+.wg-contact li {
   padding-top: 0.8rem; /* Align text with bottom of `fa-2x` icon. */
   margin-bottom: 0.3rem;
 }
 
-.contact-widget li:last-of-type {
+.wg-contact li:last-of-type {
   margin-bottom: 0;
 }
 

+ 3 - 0
wowchemy/assets/scss/wowchemy/wowchemy.scss

@@ -6,6 +6,8 @@
  **************************************************/
 
 @import "root";
+@import "callouts";
+@import "shortcodes";
 @import "icons";
 @import "footer";
 @import "nav";
@@ -18,3 +20,4 @@
 @import "dark";
 @import "integrations";
 @import "rtl";
+@import "breadcrumb";

+ 17 - 1
wowchemy/config.yaml

@@ -8,8 +8,24 @@ outputFormats:
     rel: manifest
 module:
   hugoVersion:
-    min: '0.73.0'
+    min: '0.78.2'
     extended: true
+  mounts:
+    - source: content
+      target: content
+    - source: static
+      target: static
+    - source: layouts
+      target: layouts
+    - source: data
+      target: data
+    - source: assets
+      target: assets
+    - source: i18n
+      target: i18n
+    - source: archetypes
+      target: archetypes
+taxonomies: []
 params:
   theme: minimal
   font: native

+ 11 - 0
wowchemy/data/fonts/cyberpunk.toml

@@ -0,0 +1,11 @@
+# Font style metadata
+name = "Cyberpunk"
+
+# Optional Google font URL
+google_fonts = "family=B612+Mono&family=B612:wght@400;700&family=Jura:wght@400;700"
+
+# Font families
+heading_font = "Jura"
+body_font = "B612"
+nav_font = "Jura"
+mono_font = "B612 Mono"

+ 26 - 0
wowchemy/data/themes/cyberpunk.toml

@@ -0,0 +1,26 @@
+# Theme metadata
+name = "Cyberpunk"
+
+# Is theme light or dark?
+is_light = false
+
+# Primary
+primary = "rgb(255, 0, 60)"
+
+# Menu
+menu_primary = "#fcee0a"
+menu_text = "#000"
+menu_text_active = "rgb(255, 0, 60)"
+menu_title = "#000"
+
+[dark]
+  # Menu
+  menu_primary = "#fcee0a"
+  menu_text = "#000"
+  menu_text_active = "#000"
+  menu_title = "#000"
+
+  # Backgrounds
+  background = "#000"
+  home_section_odd = "#000"
+  home_section_even = "#000"

+ 25 - 0
wowchemy/data/themes/earth.toml

@@ -0,0 +1,25 @@
+# Theme metadata
+name = "Earth"
+
+# Is theme light or dark?
+is_light = true
+
+# Primary
+primary = "#707070"
+
+# Menu
+menu_primary = "#f6eee9"
+menu_text = "#000"
+menu_text_active = "#000"
+menu_title = "#000"
+
+# Links
+link = "#707070"
+link_hover = "#000"
+link_decoration = "underline"
+
+# Home sections
+home_section_odd = "#f9f7f6"
+home_section_even = "#ffffff"
+
+font = "rose"

+ 2 - 2
wowchemy/data/themes/ocean.toml

@@ -8,9 +8,9 @@ light = true
 primary = "#3f51b5"
 
 # Menu
-menu_primary = "#3f51b5" # 500
+menu_primary = "#3f51b5" # Material 500
 menu_text = "#fff"
-menu_text_active = "#8c9eff" # A100
+menu_text_active = "#ffeb3b" # Material Complemantary 500
 menu_title = "#fff"
 
 # Home sections

+ 1 - 1
wowchemy/data/wowchemy.toml

@@ -1,3 +1,3 @@
 # Wowchemy
 
-version = "5.0.0-beta.0"
+version = "5.0.0-beta.1"

+ 2 - 0
wowchemy/i18n/da.yaml

@@ -6,6 +6,8 @@
   translation: På denne side
 - id: back_to_top
   translation: Til toppen
+- id: home
+  translation: Hjem
 - id: related
   translation: Relaterede
 - id: minute_read

+ 8 - 0
wowchemy/i18n/de.yaml

@@ -18,6 +18,14 @@
   translation: 'Abbildung %d:'
 - id: edit_page
   translation: Seite editieren
+- id: theme_selector
+  translation: Einstellungen anzeigen
+- id: theme_light
+  translation: Hell
+- id: theme_dark
+  translation: Dunkel
+- id: theme_auto
+  translation: Automatisch
 - id: btn_preprint
   translation: Vorabdruck
 - id: btn_pdf

+ 12 - 3
wowchemy/i18n/en.yaml

@@ -12,6 +12,9 @@
 - id: back_to_top
   translation: Back to top
 
+- id: home
+  translation: Home
+
 # General
 
 - id: related
@@ -34,6 +37,9 @@
 
 # Themes
 
+- id: theme_selector
+  translation: Display preferences
+
 - id: theme_light
   translation: Light
 
@@ -111,7 +117,7 @@
   translation: See all posts
 
 - id: more_talks
-  translation: See all talks
+  translation: See all events
 
 - id: more_publications
   translation: See all publications
@@ -133,7 +139,7 @@
 - id: book_appointment
   translation: Book an appointment
 
-# Publication/Talk details
+# Publication/Event details
 
 - id: abstract
   translation: Abstract
@@ -197,7 +203,7 @@
   translation: Publications
 
 - id: talks
-  translation: Talks
+  translation: Events
 
 - id: projects
   translation: Projects
@@ -219,6 +225,9 @@
 - id: search_no_results
   translation: No results found
 
+- id: search_common_queries
+  translation: Common searches
+
 # Error 404
 
 - id: page_not_found

+ 5 - 1
wowchemy/i18n/es.yaml

@@ -8,6 +8,8 @@
   translation: En esta página
 - id: back_to_top
   translation: Regreso al inicio
+- id: home
+  translation: Inicio
 
 # General
 
@@ -26,6 +28,8 @@
 
 # Themes
 
+- id: theme_selector
+  translation: Mostrar preferencias
 - id: theme_light
   translation: Claro
 - id: theme_dark
@@ -143,7 +147,7 @@
 - id: open_project_site
   translation: Ir al sitio del proyecto
 
-# Default titles for archive pages
+# Content types for default archive page titles and search results
 
 - id: posts
   translation: Posts

+ 13 - 10
wowchemy/i18n/he.yaml

@@ -4,7 +4,7 @@
   translation: ניווט
 
 - id: table_of_contents
-  translation: Table of Contents
+  translation: תוכן עניינים
 
 - id: on_this_page
   translation: תוכן
@@ -21,19 +21,22 @@
   translation: דק׳ קריאה
 
 - id: previous
-  translation: קודם
+  translation: הקודם
 
 - id: next
   translation: הבא
 
 - id: figure
-  translation: "דמות %d:"
+  translation: "איור %d:"
 
 - id: edit_page
   translation: עריכת דף זה
 
 # Themes
 
+- id: theme_selector
+  translation: העדפות תצוגה
+
 - id: theme_light
   translation: בהיר
 
@@ -46,13 +49,13 @@
 # Buttons
 
 - id: btn_preprint
-  translation: הדפסה מוקדמת
+  translation: קדם-פרסום
 
 - id: btn_pdf
   translation: PDF
 
 - id: btn_cite
-  translation: צטט
+  translation: ציטוט
 
 - id: btn_slides
   translation: שקופיות
@@ -64,7 +67,7 @@
   translation: קוד
 
 - id: btn_dataset
-  translation: מערך נתונים
+  translation: ערכת נתונים
 
 - id: btn_project
   translation: פרויקט
@@ -90,7 +93,7 @@
   translation: השכלה
 
 - id: user_profile_latest
-  translation: עדכני
+  translation: פורסם לאחרונה
 
 # Accomplishments widget
 
@@ -148,7 +151,7 @@
   translation: תאריך
 
 - id: last_updated
-  translation: עודכן לאחרונה ב
+  translation: עודכן לאחרונה ב-
 
 - id: event
   translation: אירוע
@@ -166,7 +169,7 @@
   translation: מאמר בכתב עת
 
 - id: pub_preprint
-  translation: הדפסה מוקדמת
+  translation: קדם-פרסום
 
 - id: pub_report
   translation: דו״ח
@@ -230,7 +233,7 @@
 # Cookie consent
 
 - id: cookie_message
-  translation: אתר זה משתמש בקובצי Cookie כדי להבטיח שתקבל את החוויה הטובה ביותר באתר.
+  translation: אתר זה משתמש בקובצי Cookie כדי להבטיח שתקבלו את החוויה הטובה ביותר באתר.
 
 - id: cookie_dismiss
   translation: הבנתי!

+ 34 - 23
wowchemy/layouts/_default/baseof.html

@@ -6,37 +6,48 @@
 
 {{ $show_navbar := site.Params.main_menu.enable | default true }}
 {{- $highlight_active_link := site.Params.main_menu.highlight_active_link | default true -}}
-<body id="top" data-spy="scroll" {{ if $show_navbar }}data-offset="70"{{end}} data-target="{{ if or .IsHome (eq .Type "widget_page") | and $highlight_active_link }}#navbar-main{{else}}#TableOfContents{{end}}" class="{{ if not (.Scratch.Get "light") }}dark{{end}} {{ if not $show_navbar }}no-navbar{{end}}">
-
-  {{/* Load day/night theme. */}}
-  {{/* Initialise default theme. */}}
-  {{ if site.Params.day_night }}
-    <script>window.wcDarkLightEnabled = true;</script>
-  {{ end }}
-  {{ if eq (.Scratch.Get "light") true }}
-    <script>const isSiteThemeDark = false;</script>
-  {{ else }}
-    <script>const isSiteThemeDark = true;</script>
-  {{ end }}
-  {{ $load_theme := resources.Get "js/load-theme.js" }}
-  <script src="{{ $load_theme.RelPermalink }}"></script>
+<body id="top" data-spy="scroll" {{ if $show_navbar }}data-offset="70"{{end}} data-target="{{ if or .IsHome (eq .Type "widget_page") | and $highlight_active_link }}#navbar-main{{else}}#TableOfContents{{end}}" class="page-wrapper {{ if not (.Scratch.Get "light") }}dark{{end}} {{ if not $show_navbar }}no-navbar{{end}}">
+
+  {{/* Initialise Wowchemy. */}}
+  {{ $js_license := printf "/*! Wowchemy v%s | https://wowchemy.com/ */\n" site.Data.wowchemy.version }}
+  {{ $js_license := $js_license | printf "%s/*! Copyright 2016-present George Cushen (https://georgecushen.com/) */\n" }}
+  {{ $js_license := $js_license | printf "%s/*! License: https://github.com/wowchemy/wowchemy-hugo-modules/blob/master/LICENSE.md */\n" }}
+  {{ $js_bundle_head := $js_license | resources.FromString "js/bundle-head.js" }}
+  {{ $wcDarkLightEnabled := site.Params.day_night | default false }}
+  {{ $wcIsSiteThemeDark := not (.Scratch.Get "light") | default false }}
+  {{ $js_params := dict "wcDarkLightEnabled" $wcDarkLightEnabled "wcIsSiteThemeDark" $wcIsSiteThemeDark }}
+  {{ $js_bundle := resources.Get "js/wowchemy-init.js" | js.Build (dict "params" $js_params) }}
+  {{- if eq hugo.Environment "production" -}}
+    {{ $js_bundle = $js_bundle | minify }}
+  {{- end -}}
+  {{ $js_bundle := slice $js_bundle_head $js_bundle | resources.Concat "js/wowchemy-init.min.js" }}
+  {{- if eq hugo.Environment "production" -}}
+    {{- $js_bundle = $js_bundle | fingerprint "md5" -}}
+  {{- end -}}
+  <script src="{{ $js_bundle.RelPermalink }}"></script>
 
   {{ partial "search" . }}
 
-  {{ partial "navbar" . }}
-
-  {{ block "main" . }}{{ end }}
+  <div class="page-header">
+    {{ partial "navbar" . }}
+  </div>
 
-  {{ partial "site_js" . }}
+  <div class="page-body">
+    {{ block "main" . }}{{ end }}
+  </div>
 
-  {{/* Docs and Updates layouts include the site footer in a different location. */}}
-  {{ if not (in (slice "book" "docs" "updates") .Type) }}
-  <div class="container">
-    {{ partial "site_footer" . }}
+  <div class="page-footer">
+    {{/* Docs and Updates layouts include the site footer in a different location. */}}
+    {{ if not (in (slice "book" "docs" "updates") .Type) }}
+    <div class="container">
+      {{ partial "site_footer" . }}
+    </div>
+    {{ end }}
   </div>
-  {{ end }}
 
   {{ partial "citation" . }}
 
+  {{ partial "site_js" . }}
+
 </body>
 </html>

+ 0 - 0
wowchemy/layouts/talk/single.html → wowchemy/layouts/event/single.html


+ 8 - 0
wowchemy/layouts/partials/book_layout.html

@@ -1,5 +1,9 @@
 {{ $current_page := . }}
 
+{{/* Check whether to show breadcrumb navigation. */}}
+{{ $breadcrumb_page_types := site.Params.breadcrumb.page_types | default dict }}
+{{ $show_breadcrumb := index $breadcrumb_page_types .Type | default false }}
+
 <div class="container-fluid docs">
   <div class="row flex-xl-nowrap">
     <div class="col-12 col-md-3 col-xl-2 docs-sidebar">
@@ -24,6 +28,10 @@
       <article class="article">
 
         <div class="docs-article-container">
+          {{ if $show_breadcrumb }}
+            {{ partial "breadcrumb" $current_page }}
+          {{ end }}
+
           <h1>{{ .Title }}</h1>
 
           <div class="article-style">

+ 2 - 2
wowchemy/layouts/partials/book_menu.html

@@ -7,9 +7,9 @@
 {{ $icon := "" }}
 
 {{ with .sect }}
-  {{ if .IsSection}}
+  {{ if .IsSection }}
     {{ if not $is_root }}
-      {{- $first = eq $current_node.FirstSection . -}}
+      {{- $first = (eq $current_node.FirstSection.Type "book") | and (eq $current_node.FirstSection .) -}}
     {{ end }}
 
     {{- safeHTML $current_node.FirstSection.Params.pre_nav -}}

+ 42 - 15
wowchemy/layouts/partials/book_sidebar.html

@@ -15,6 +15,8 @@
   {{ $query := "" }}
   {{ $root_page := .GetPage "/_index.md" }}
   {{ $is_root := false}}
+
+  {{/* Case where homepage is a book */}}
   {{ if $root_page | and (eq $root_page.Type "book") }}
     {{ $is_root = true}}
     <ul class="nav docs-sidenav">
@@ -27,23 +29,48 @@
     {{- else -}}
       {{- $query = .Site.Home.Sections.ByWeight -}}
     {{- end}}
+
   {{else}}
-    {{/* Get section name from the path. */}}
-    {{ $menu_name = (path.Base (path.Split .FirstSection).Dir) }}
+    {{/* Case where homepage is NOT a book */}}
 
-    {{/* For any folder named `updates`, use descending title order (e.g. latest release note first). */}}
-    {{ $order_by = cond (eq $menu_name "updates") "title_desc" $order_by }}
+    {{ if eq .FirstSection.Type "book" }}
+      {{/* Case where first section is a book. */}}
 
-    {{- if eq $order_by "title" -}}
-      {{- $query = where .Site.Home.Sections.ByTitle "Section" $menu_name -}}
-    {{- else if eq $order_by "title_desc" -}}
-      {{- $query = where .Site.Home.Sections.ByTitle.Reverse "Section" $menu_name -}}
-    {{- else -}}
-      {{- $query = where .Site.Home.Sections.ByWeight "Section" $menu_name -}}
-    {{- end}}
-  {{end}}
+      {{ $menu_name = (path.Base (path.Split .FirstSection).Dir) }}
+      {{/* For any folder named `updates`, use descending title order (e.g. latest release note first). */}}
+      {{ $order_by = cond (eq $menu_name "updates") "title_desc" $order_by }}
+      {{- if eq $order_by "title" -}}
+        {{- $query = where .Site.Home.Sections.ByTitle "Section" $menu_name -}}
+      {{- else if eq $order_by "title_desc" -}}
+        {{- $query = where .Site.Home.Sections.ByTitle.Reverse "Section" $menu_name -}}
+      {{- else -}}
+        {{- $query = where .Site.Home.Sections.ByWeight "Section" $menu_name -}}
+      {{- end -}}
+
+      {{- range $query -}}
+        {{ template "book-menu" dict "sect" . "current_node" $current_node "order_by" $order_by "is_root" $is_root }}
+      {{- end -}}
+
+    {{ else }}
+      {{/* Case where first section is a general page (e.g. book of books). */}}
 
-  {{- range $query -}}
-    {{ template "book-menu" dict "sect" . "current_node" $current_node "order_by" $order_by "is_root" $is_root }}
-  {{- end -}}
+      {{ $first_node := $current_node }}
+      {{ if ne .Parent .FirstSection }}
+        {{ if ne .Parent.Parent .FirstSection }}
+          {{ $first_node = $current_node.Parent.Parent }}
+        {{else}}
+          {{ $first_node = $current_node.Parent }}
+        {{end}}
+      {{end}}
+
+      {{- if eq $order_by "title" -}}
+        {{- $query = $first_node.Pages.ByTitle -}}
+      {{- else if eq $order_by "title_desc" -}}
+        {{- $query =  $first_node.Pages.ByTitle.Reverse -}}
+      {{- else -}}
+        {{- $query =  $first_node.Pages.ByWeight -}}
+      {{- end -}}
+      {{ template "book-menu" dict "sect" $first_node "current_node" $current_node "order_by" $order_by "is_root" $is_root }}
+    {{end}}
+  {{end}}
 </nav>

+ 10 - 0
wowchemy/layouts/partials/breadcrumb.html

@@ -0,0 +1,10 @@
+{{ if not .IsHome }}
+  <nav aria-label="breadcrumb">
+    <ol class="breadcrumb">
+      {{ partial "breadcrumb_helper" . }}
+      <li class="breadcrumb-item active" aria-current="page">
+        {{ (.LinkTitle | default .Title) | emojify }}
+      </li>
+    </ol>
+  </nav>
+{{ end }}

+ 12 - 0
wowchemy/layouts/partials/breadcrumb_helper.html

@@ -0,0 +1,12 @@
+{{ with .Parent }}
+  {{ partial "breadcrumb_helper" . }}
+  <li class="breadcrumb-item">
+    <a href="{{ .RelPermalink }}">
+      {{ if .IsHome }}
+        {{ i18n "home" | default "Home" }}
+      {{ else }}
+        {{ (.LinkTitle | default .Title) | emojify }}
+      {{ end }}
+    </a>
+  </li>
+{{ end }}

+ 37 - 0
wowchemy/layouts/partials/functions/get_social_link.html

@@ -0,0 +1,37 @@
+{{/* Function to return a linked social icon as a map from an iteration of an author's `social` data. */}}
+
+{{ $scr := newScratch }}
+
+{{/* Get icon name. */}}
+{{ $scr.SetInMap "social_link" "icon" .icon }}
+
+{{/* Get icon pack (default to Font Awesome's Solid pack). */}}
+{{ $pack := or .icon_pack "fas" }}
+{{ $scr.SetInMap "social_link" "icon_pack" $pack }}
+
+{{/* Derive Font Awesome class name prefix. */}}
+{{ $pack_prefix := $pack }}
+{{ if in (slice "fab" "fas" "far" "fal") $pack }}
+  {{ $pack_prefix = "fa" }}
+{{ end }}
+{{ $scr.SetInMap "social_link" "pack_prefix" $pack_prefix }}
+
+{{/* Get tooltip label (default to none). */}}
+{{ $scr.SetInMap "social_link" "tooltip" (.label | default "") }}
+
+{{/* Get screen reader label (default to icon name). */}}
+{{ $scr.SetInMap "social_link" "aria_label" (.label | default .icon) }}
+
+{{/* Get external link or relative internal link. */}}
+{{ $link := .link }}
+{{ $target := "" }}
+{{ $scheme := (urls.Parse $link).Scheme }}
+{{ if not $scheme }}
+  {{ $link = .link | relLangURL }}
+{{ else if in (slice "http" "https") $scheme }}
+  {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
+{{ end }}
+{{ $scr.SetInMap "social_link" "link" $link }}
+{{ $scr.SetInMap "social_link" "target" $target }}
+
+{{ return $scr.Get "social_link" }}

+ 2 - 0
wowchemy/layouts/partials/functions/parse_theme.html

@@ -59,6 +59,8 @@
 {{- $scr.Set "dark_link" ($theme.dark.link | default $theme.primary) -}}
 {{- $scr.Set "dark_link_hover" ($theme.dark.link_hover | default $theme.primary) -}}
 
+{{- $scr.Set "link_decoration" ($theme.link_decoration | default "inherit") -}}
+
 {{- $scr.Set "primary" $theme.primary -}}
 
 {{- $scr.Set "menu_primary" $theme.menu_primary -}}

+ 2 - 2
wowchemy/layouts/partials/jsonld/main.html

@@ -4,7 +4,7 @@
 
 {{- if $page.IsHome -}}
 
-  {{ partial "jsonld/website.html" $page }}
+  {{ partialCached "jsonld/website.html" $page }}
 
   {{ if ne $site_type "Person" }}
     {{ partial "jsonld/business.html" $page }}
@@ -16,7 +16,7 @@
     {{ partial "jsonld/article.html" (dict "page" $page "summary" $summary) }}
   {{ end }}
 
-  {{ if eq $page.Type "talk" }}
+  {{ if eq $page.Type "event" }}
     {{ partial "jsonld/event.html" (dict "page" $page "summary" $summary) }}
   {{ end }}
 

+ 6 - 4
wowchemy/layouts/partials/li_card.html

@@ -3,7 +3,7 @@
 
 {{/* Dynamic view adjusts to content type. */}}
 {{ $show_buttons := false }}
-{{ if eq $item.Type "talk" }}
+{{ if eq $item.Type "event" }}
   {{ $show_buttons = true }}
 {{ else if eq $item.Type "publication" }}
   {{ $show_buttons = true }}
@@ -21,7 +21,7 @@
 
 <div class="card-simple">
 
-  {{ if eq $item.Type "talk" }}
+  {{ if eq $item.Type "event" }}
   <div class="article-metadata">
     {{ if $item.Params.authors }}
     <div>
@@ -43,9 +43,11 @@
   {{ $resource := ($item.Resources.ByType "image").GetMatch "*featured*" }}
   {{ $anchor := $item.Params.image.focal_point | default "Smart" }}
   {{ with $resource }}
-  {{ $image := .Fill (printf "918x517 q90 %s" $anchor) }}
+  {{ $filters := slice (images.GaussianBlur 21) (images.Pixelate 8) }}
+  {{ $image := .Fill (printf "808x455 %s" $anchor) }}
+  {{ $image_lq := (.Fill (printf "808x455 %s q1" $anchor)).Filter $filters }}
   <a href="{{ $item.RelPermalink }}">
-      <img src="{{ $image.RelPermalink }}" class="article-banner" alt="{{ $item.Title }}">
+    <div class="img-hover-zoom"><img src="{{ $image_lq.RelPermalink }}" data-src="{{ $image.RelPermalink }}" class="article-banner lazyload" alt="{{ $item.Title }}"></div>
   </a>
   {{end}}
 

+ 6 - 4
wowchemy/layouts/partials/li_compact.html

@@ -9,7 +9,7 @@
   {{ $link = $item.Params.external_link }}
   {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
 {{ end }}
-{{ if eq $item.Type "talk" }}
+{{ if eq $item.Type "event" }}
   {{ $show_authors_only = true }}
   {{ $show_buttons = true }}
 {{ else if eq $item.Type "publication" }}
@@ -38,7 +38,7 @@
     </h3>
 
     {{ with $summary }}
-    <a href="{{ $item.RelPermalink }}" class="summary-link">
+    <a href="{{ $link }}" {{ $target | safeHTMLAttr }} class="summary-link">
       <div class="article-style">
         {{.}}
       </div>
@@ -47,7 +47,7 @@
 
     <div class="stream-meta article-metadata">
 
-      {{ if eq $item.Type "talk" }}
+      {{ if eq $item.Type "event" }}
       <div>
         <span>
           {{ partial "functions/get_event_dates" $item }}
@@ -78,9 +78,11 @@
   <div class="ml-3">
     {{ $resource := ($item.Resources.ByType "image").GetMatch "*featured*" }}
     {{ with $resource }}
+    {{ $filters := slice (images.GaussianBlur 21) (images.Pixelate 8) }}
     {{ $image := .Resize "150x" }}
+    {{ $image_lq := (.Resize "150x q1").Filter $filters }}
     <a href="{{$link}}" {{ $target | safeHTMLAttr }}>
-      <img src="{{ $image.RelPermalink }}" alt="{{ $item.Title }}">
+      <img src="{{ $image_lq.RelPermalink }}" data-src="{{ $image.RelPermalink }}" alt="{{ $item.Title }}" class="lazyload">
     </a>
     {{end}}
   </div>

+ 9 - 0
wowchemy/layouts/partials/marketing/microsoft_clarity.html

@@ -0,0 +1,9 @@
+{{ if (in (slice (getenv "HUGO_ENV") hugo.Environment) "production") | and site.Params.marketing.microsoft_clarity }}
+<script>
+  (function(c,l,a,r,i,t,y){
+      c[a]=c[a]||function(){(c[a].q=c[a].q||[]).push(arguments)};
+      t=l.createElement(r);t.async=1;t.src="https://www.clarity.ms/tag/"+i;
+      y=l.getElementsByTagName(r)[0];y.parentNode.insertBefore(t,y);
+  })(window, document, "clarity", "script", '{{site.Params.marketing.microsoft_clarity}}');
+</script>
+{{ end }}

+ 14 - 1
wowchemy/layouts/partials/navbar.html

@@ -139,6 +139,19 @@
     </div><!-- /.navbar-collapse -->
 
     <ul class="nav-icons navbar-nav flex-row ml-auto d-flex pl-md-2">
+
+      {{/* Display any social links that the superuser chose to display in the header. */}}
+      {{ range where (where (where site.Pages "Section" "authors") ".Params.superuser" true) ".Params.social" "!=" nil }}
+        {{ range where .Params.social ".display.header" true }}
+          {{ $social_link := partial "functions/get_social_link" . }}
+          <li class="nav-item d-none d-lg-inline-flex">
+            <a class="nav-link" href="{{ $social_link.link | safeURL }}"{{ with $social_link.tooltip }} data-toggle="tooltip" data-placement="bottom" title="{{.}}"{{ end }} {{ $social_link.target | safeHTMLAttr }} aria-label="{{ $social_link.aria_label }}">
+              <i class="{{ $social_link.icon_pack }} {{ $social_link.pack_prefix }}-{{ $social_link.icon }}" aria-hidden="true"></i>
+            </a>
+          </li>
+        {{ end }}
+      {{ end }}
+
       {{ $show_search := site.Params.main_menu.show_search | default true }}
       {{ if and site.Params.search.engine $show_search }}
       <li class="nav-item">
@@ -149,7 +162,7 @@
       {{ $show_day_night := site.Params.main_menu.show_day_night | default true }}
       {{ if and site.Params.day_night $show_day_night }}
       <li class="nav-item dropdown theme-dropdown">
-        <a href="#" class="nav-link" data-toggle="dropdown" aria-haspopup="true">
+        <a href="#" class="nav-link" data-toggle="dropdown" aria-haspopup="true" aria-label="{{ i18n "theme_selector" | default "Display preferences" }}">
           <i class="fas fa-moon" aria-hidden="true"></i>
         </a>
         <div class="dropdown-menu">

+ 3 - 3
wowchemy/layouts/partials/page_author_card.html

@@ -14,14 +14,14 @@
   {{ $avatar_shape := site.Params.avatar.shape | default "circle" }}
   <div class="media author-card content-widget-hr">
     {{ if and site.Params.avatar.gravatar $author_page.Params.email }}
-      <a href="{{$profile_url}}"><img class="avatar mr-3 {{if eq $avatar_shape "square"}}avatar-square{{else}}avatar-circle{{end}}" src="https://s.gravatar.com/avatar/{{ md5 $author_page.Params.email }}?s=200')" alt="{{$author_page.Title}}"></a>
+      {{if $profile_url}}<a href="{{$profile_url}}">{{end}}<img class="avatar mr-3 {{if eq $avatar_shape "square"}}avatar-square{{else}}avatar-circle{{end}}" src="https://s.gravatar.com/avatar/{{ md5 $author_page.Params.email }}?s=200" alt="{{$author_page.Title}}">{{if $profile_url}}</a>{{end}}
     {{ else if $avatar }}
       {{ $avatar_image := $avatar.Fill "270x270 Center" }}
-      <a href="{{$profile_url}}"><img class="avatar mr-3 {{if eq $avatar_shape "square"}}avatar-square{{else}}avatar-circle{{end}}" src="{{ $avatar_image.RelPermalink }}" alt="{{$author_page.Title}}"></a>
+      {{if $profile_url}}<a href="{{$profile_url}}">{{end}}<img class="avatar mr-3 {{if eq $avatar_shape "square"}}avatar-square{{else}}avatar-circle{{end}}" src="{{ $avatar_image.RelPermalink }}" alt="{{$author_page.Title}}">{{if $profile_url}}</a>{{end}}
     {{ end }}
 
     <div class="media-body">
-      <h5 class="card-title"><a href="{{$profile_url}}">{{$author_page.Title}}</a></h5>
+      <h5 class="card-title">{{if $profile_url}}<a href="{{$profile_url}}">{{end}}{{$author_page.Title}}{{if $profile_url}}</a>{{end}}</h5>
       {{ with $author_page.Params.role }}<h6 class="card-subtitle">{{. | markdownify | emojify}}</h6>{{end}}
       {{ with $author_page.Params.bio }}<p class="card-text">{{. | markdownify | emojify}}</p>{{end}}
       {{ partial "social_links" $author_page }}

+ 1 - 1
wowchemy/layouts/partials/page_header.html

@@ -7,7 +7,7 @@
 {{ if and (not $title) .IsNode }}
   {{ if eq .Type "post" }}
     {{ $title = i18n "posts" }}
-  {{ else if eq .Type "talk" }}
+  {{ else if eq .Type "event" }}
     {{ $title = i18n "talks" }}
   {{ else if eq .Type "publication" }}
     {{ $title = i18n "publications" }}

+ 1 - 1
wowchemy/layouts/partials/page_metadata.html

@@ -13,7 +13,7 @@
   {{ end }}
   {{ end }}
 
-  {{ if not (in (slice "talk" "page") $page.Type) }}
+  {{ if not (in (slice "event" "page") $page.Type) }}
   <span class="article-date">
     {{ $date := $page.Lastmod.Format site.Params.date_format }}
     {{ if eq $page.Type "publication" }}

+ 2 - 2
wowchemy/layouts/partials/page_metadata_authors.html

@@ -2,14 +2,14 @@
 
 {{- $taxonomy := "authors" }}
 {{ if .Param $taxonomy }}
-  {{ $link_authors := site.Params.link_authors | default true }}
   {{ range $index, $value := (.GetTerms $taxonomy) }}
     {{- /* Highlight the author's name? */ -}}
     {{- $highlight_name := .Page.Params.highlight_name | default false -}}
 
     {{- if gt $index 0 }}, {{ end -}}
     <span {{ if $highlight_name }}class="author-highlighted"{{end}}>
-      {{- if $link_authors -}}
+      {{/* Effectively check the page's `_build` option as `_build` is not exposed in Hugo's Page object. */}}
+      {{- if .RelPermalink -}}
         <a href="{{.RelPermalink}}">{{.LinkTitle}}</a>
       {{- else -}}
         {{ .LinkTitle }}

+ 4 - 2
wowchemy/layouts/partials/portfolio_li_card.html

@@ -20,9 +20,11 @@
 <div class="project-card project-item isotope-item {{ $js_tag_classes | safeHTMLAttr }}">
   <div class="card">
     {{ with $resource }}
-    {{ $image := .Resize (printf "550x q90 %s") }}
+    {{ $filters := slice (images.GaussianBlur 21) (images.Pixelate 8) }}
+    {{ $image := .Resize "550x" }}
+    {{ $image_lq := (.Resize "550x q1").Filter $filters }}
     <a href="{{ $link }}" {{ $target | safeHTMLAttr }} class="card-image hover-overlay">
-      <img src="{{ $image.RelPermalink }}" alt="" class="img-responsive">
+      <img src="{{ $image_lq.RelPermalink }}" data-src="{{ $image.RelPermalink }}" alt="{{ $item.Title }}" class="img-responsive lazyload">
     </a>
     {{ end }}
     <div class="card-text">

+ 3 - 1
wowchemy/layouts/partials/portfolio_li_showcase.html

@@ -51,9 +51,11 @@
     <div class="col-12 col-md-6 order-first {{$order}}">
       {{ $resource := ($item.Resources.ByType "image").GetMatch "*featured*" }}
       {{ with $resource }}
+      {{ $filters := slice (images.GaussianBlur 21) (images.Pixelate 8) }}
       {{ $image := .Resize "540x" }}
+      {{ $image_lq := (.Resize "540x q1").Filter $filters }}
       {{if $do_link}}<a href="{{ $link }}" {{ $target | safeHTMLAttr }}>{{end}}
-        <img src="{{ $image.RelPermalink }}" alt="">
+        <img src="{{ $image_lq.RelPermalink }}" data-src="{{ $image.RelPermalink }}" alt="{{ $item.Title }}" class="lazyload">
       {{if $do_link}}</a>{{end}}
       {{end}}
     </div>

+ 16 - 0
wowchemy/layouts/partials/search.html

@@ -1,3 +1,5 @@
+{{/* Partial for built-in search and Algolia search. */}}
+{{ if eq site.Params.search.engine 1 | or (eq site.Params.search.engine 2) }}
 <aside class="search-results" id="search">
   <div class="container">
     <section class="search-header">
@@ -20,6 +22,19 @@
         {{ end }}
       </div>
 
+      {{ if eq site.Params.search.engine 1 | and site.Data.search_queries }}
+      <div id="search-common-queries" class="pt-3">
+        <div class="font-weight-bold pb-3">{{ i18n "search_common_queries" | default "Common searches" }}</div>
+        <ul class="fa-ul">
+          {{ range site.Data.search_queries }}
+            <li class="pb-3">
+              <a href="{{.link | relURL}}"><i class="fa-li fas fa-search" aria-hidden="true"></i><span class="pl-1">{{.query | markdownify | emojify}}</span></a>
+            </li>
+          {{ end }}
+        </ul>
+      </div>
+      {{ end }}
+
     </section>
     <section class="section-search-results">
 
@@ -30,3 +45,4 @@
     </section>
   </div>
 </aside>
+{{end}}

+ 1 - 11
wowchemy/layouts/partials/site_footer.html

@@ -23,19 +23,9 @@
     {{ $hide_published_with_footer := site.Params.power_ups.hide_published_with | default true }}
     {{ if not (and $is_sponsor $hide_published_with_footer) }}
     Published with
-    <a href="https://wowchemy.com" target="_blank" rel="noopener">Wowchemy</a>  —
+    <a href="https://wowchemy.com/?utm_campaign=poweredby" target="_blank" rel="noopener">Wowchemy</a>  —
     the free, <a href="https://github.com/wowchemy/wowchemy-hugo-modules" target="_blank" rel="noopener">
     open source</a> website builder that empowers creators.
     {{ end }}
-
-    {{ if not (in (slice "book" "docs" "updates") .Type) }}
-    <span class="float-right">
-      <a href="#" class="back-to-top" aria-label="Back to top">
-        <span class="button_icon">
-          <i class="fas fa-chevron-up fa-2x"></i>
-        </span>
-      </a>
-    </span>
-    {{ end }}
   </p>
 </footer>

+ 6 - 6
wowchemy/layouts/partials/site_footer_license.html

@@ -28,16 +28,16 @@
   {{ end }}
 
   <p class="powered-by footer-license-icons">
-    <a href="{{$license_url}}" rel="noopener noreferrer" target="_blank">
-      <img src="https://search.creativecommons.org/static/img/cc_icon.svg" alt="CC icon">
-      <img src="https://search.creativecommons.org/static/img/cc-by_icon.svg" alt="CC by icon">
+    <a href="{{$license_url}}" rel="noopener noreferrer" target="_blank" aria-label="Creative Commons">
+      <i class="fab fa-creative-commons fa-2x" aria-hidden="true"></i>
+      <i class="fab fa-creative-commons-by fa-2x" aria-hidden="true"></i>
       {{ if not $allow_commercial }}
-        <img src="https://search.creativecommons.org/static/img/cc-nc_icon.svg" alt="CC NC icon">
+        <i class="fab fa-creative-commons-nc fa-2x" aria-hidden="true"></i>
       {{end}}
       {{ if and $allow_derivatives $share_alike }}
-        <img src="https://search.creativecommons.org/static/img/cc-sa_icon.svg" alt="CC SA icon">
+        <i class="fab fa-creative-commons-sa fa-2x" aria-hidden="true"></i>
       {{ else if not $allow_derivatives }}
-        <img src="https://search.creativecommons.org/static/img/cc-nd_icon.svg" alt="CC ND icon">
+        <i class="fab fa-creative-commons-nd fa-2x" aria-hidden="true"></i>
       {{end}}
     </a>
   </p>

+ 14 - 7
wowchemy/layouts/partials/site_head.html

@@ -142,22 +142,28 @@
     {{ end }}
   {{ end }}
 
-  {{ $css_comment := printf "/*!* Wowchemy v%s (https://wowchemy.com/) */\n" site.Data.wowchemy.version }}
-  {{ $css_bundle_head := $css_comment | resources.FromString "css/bundle-head.css" }}
+  {{ $license := printf "/*! Wowchemy v%s | https://wowchemy.com/ */\n" site.Data.wowchemy.version }}
+  {{ $license := $license | printf "%s/*! Copyright 2016-present George Cushen (https://georgecushen.com/) */\n" }}
+  {{ $license := $license | printf "%s/*! License: https://github.com/wowchemy/wowchemy-hugo-modules/blob/master/LICENSE.md */\n" }}
+  {{ $css_bundle_head := $license | resources.FromString "css/bundle-head.css" }}
   {{ $css_options := dict "targetPath" "css/wowchemy.css" }}
-  {{- if (in (slice (getenv "HUGO_ENV") hugo.Environment) "production") -}}
+  {{- if eq hugo.Environment "production" -}}
     {{- $css_options = merge $css_options (dict "outputStyle" "compressed") -}}
   {{- end -}}
   {{ $sass_template := resources.Get "scss/main.scss" }}
   {{ $style := $sass_template | resources.ExecuteAsTemplate "main_parsed.scss" . | toCSS $css_options }}
+  {{- if eq hugo.Environment "production" -}}
+    {{- $style = $style | minify -}}
+  {{- end -}}
   {{ $style := slice $css_bundle_head $style | resources.Concat "css/wowchemy.css" }}
-  {{- if (eq (getenv "HUGO_ENV") "production") -}}
-    {{- $style = $style | minify | fingerprint "md5" -}}
+  {{- if eq hugo.Environment "production" -}}
+    {{- $style = $style | fingerprint "md5" -}}
   {{- end -}}
   <link rel="stylesheet" href="{{ $style.RelPermalink }}">
 
   {{ partial "marketing/google_analytics" . }}
   {{ partial "marketing/google_tag_manager" . }}
+  {{ partial "marketing/microsoft_clarity" . }}
 
   {{/* Netlify Identity integration. */}}
   {{ if .IsHome | and (site.Params.cms.netlify_cms | default false) }}
@@ -180,8 +186,9 @@
   {{ $has_logo := fileExists "assets/images/logo.png" | or (fileExists "assets/images/logo.svg") }}
   {{ $og_image := "" }}
   {{ $twitter_card := "summary_large_image" }}
-  {{ if (and (eq .Kind "taxonomy") $avatar_image) }}
-    {{ $og_image = ($avatar_image.Fill "270x270 Center").Permalink }}{{/* Match image proc in About widget. */}}
+  {{ if (and (eq .Kind "term") $avatar_image) }}
+    {{/* Match image processing in About widget to prevent generating more images than necessary. */}}
+    {{ $og_image = ($avatar_image.Fill "270x270 Center").Permalink }}
     {{ $twitter_card = "summary" }}
   {{ else if $featured_image }}
     {{ $og_image = $featured_image.Permalink }}

+ 21 - 11
wowchemy/layouts/partials/site_js.html

@@ -49,8 +49,8 @@
 
     {{ if ne site.Params.search.engine 0 }}
     {{/* Configure search engine. */}}
-    {{ $min_length := site.Params.search.academic.min_length | default 1 }}
-    {{ $threshold := site.Params.search.academic.threshold | default 0.3 }}
+    {{ $min_length := site.Params.search.wowchemy.min_length | default 1 }}
+    {{ $threshold := site.Params.search.wowchemy.threshold | default 0.3 }}
     {{ $search_i18n := dict "placeholder" (i18n "search_placeholder") "results" (i18n "search_results") "no_results" (i18n "search_no_results") }}
     {{ $search_config := dict "indexURI" ("/index.json" | relLangURL) "threshold" $threshold "minLength" $min_length }}
     <script>
@@ -60,7 +60,7 @@
         'post': {{ i18n "posts" }},
         'project': {{ i18n "projects" }},
         'publication' : {{ i18n "publications" }},
-        'talk' : {{ i18n "talks" }},
+        'event' : {{ i18n "talks" }},
         'slides' : {{ i18n "slides" | default (i18n "btn_slides") }}
         };
     </script>
@@ -124,7 +124,8 @@
     {{ end }}
 
     {{/* Netlify Identity integration. */}}
-    {{ if .IsHome | and (site.Params.cms.netlify_cms | default true) }}
+    {{/* Complements loading of Netlify JS in `site_head`. */}}
+    {{ if .IsHome | and (site.Params.cms.netlify_cms | default false) }}
     <script>
       if (window.netlifyIdentity) {
         window.netlifyIdentity.on("init", user => {
@@ -151,24 +152,33 @@
     <script id="dsq-count-scr" src="https://{{site.Params.comments.disqus.shortname}}.disqus.com/count.js" async></script>
     {{ end }}
 
-    {{ $js_comment := printf "/* Wowchemy v%s | https://wowchemy.com/ */\n" site.Data.wowchemy.version }}
-    {{ $js_bundle_head := $js_comment | resources.FromString "js/bundle-head.js" }}
+    {{ $js_license := printf "/*! Wowchemy v%s | https://wowchemy.com/ */\n" site.Data.wowchemy.version }}
+    {{ $js_license := $js_license | printf "%s/*! Copyright 2016-present George Cushen (https://georgecushen.com/) */\n" }}
+    {{ $js_license := $js_license | printf "%s/*! License: https://github.com/wowchemy/wowchemy-hugo-modules/blob/master/LICENSE.md */\n" }}
+    {{ $js_bundle_head := $js_license | resources.FromString "js/bundle-head.js" }}
     {{ $js_linebreak := "\n" | resources.FromString "js/linebreak.js" }}{{/* Fix no line break after Bootstrap JS causing error. */}}
-    {{ $js_academic := resources.Get "js/wowchemy.js" }}
-    {{ $js_academic_search := resources.Get "js/wowchemy-search.js" }}
-    {{ $js_algolia_search := resources.Get "js/algolia-search.js" }}
+    {{ $js_params := dict "hugoEnvironment" hugo.Environment }}
+    {{ $js_academic := resources.Get "js/wowchemy.js" | js.Build (dict "params" $js_params) }}
     {{ $js_bootstrap := resources.Get "js/_vendor/bootstrap.bundle.js" }}
     {{ $js_bundle := slice $js_bootstrap $js_linebreak $js_academic }}
     {{ if eq site.Params.search.engine 1 }}
+      {{ $js_academic_search := resources.Get "js/wowchemy-search.js" }}
       {{ $js_bundle = $js_bundle | append $js_academic_search }}
     {{ else if eq site.Params.search.engine 2 }}
+      {{ $js_algolia_search := resources.Get "js/algolia-search.js" }}
       {{ $js_bundle = $js_bundle | append $js_algolia_search }}
     {{ end }}
     {{ range site.Params.plugins_js }}
       {{ $js_bundle = $js_bundle | append (resources.Get (printf "js/%s.js" .)) }}
     {{ end }}
-    {{ $js_bundle := $js_bundle | resources.Concat "js/wowchemy-bundle-pre.js" | minify }}
-    {{ $js_bundle := slice $js_bundle_head $js_bundle | resources.Concat "js/wowchemy.min.js" | fingerprint "md5" }}
+    {{ $js_bundle := $js_bundle | resources.Concat "js/wowchemy-bundle-pre.js" }}
+    {{- if eq hugo.Environment "production" -}}
+      {{ $js_bundle = $js_bundle | minify }}
+    {{- end -}}
+    {{ $js_bundle := slice $js_bundle_head $js_bundle | resources.Concat "js/wowchemy.min.js" }}
+    {{- if eq hugo.Environment "production" -}}
+      {{- $js_bundle = $js_bundle | fingerprint "md5" -}}
+    {{- end -}}
     <script src="{{ $js_bundle.RelPermalink }}"></script>
 
     {{ partial "custom_js" . }}

+ 40 - 11
wowchemy/layouts/partials/widget_page.html

@@ -9,17 +9,17 @@
   {{ $headless_bundle = site.GetPage $page }}
   {{/* Check homepage exists */}}
   {{ if not $headless_bundle }}
-    {{ errorf "Homepage not found at %s!" $page }}
-    {{ errorf "Add the `/home/index.md` homepage file to each language's content folder. For example, your site should have a `content/home/` folder containing `index.md` and your homepage sections, or for multi-language sites, `content/en/home/` and `content/zh/home/` etc. Refer to the 'Build Your Homepage' and 'Language' documentation at https://wowchemy.com/docs/ and the example homepage at https://github.com/wowchemy/starter-academic/tree/master/exampleSite/content/home/index.md ." }}
+    {{ warnf "Homepage not found at %s!" $page }}
+    {{ warnf "Add the `/home/index.md` homepage file to each language's content folder. For example, your site should have a `content/home/` folder containing `index.md` and your homepage sections, or for multi-language sites, `content/en/home/` and `content/zh/home/` etc. Refer to the 'Build Your Homepage' and 'Language' documentation at https://wowchemy.com/docs/ and the example homepage at https://github.com/wowchemy/starter-academic/tree/master/exampleSite/content/home/index.md ." }}
   {{ end }}
 {{ else }}
   {{ $page = .File.Path }}
   {{ $headless_bundle = site.GetPage $page }}
   {{/* Check widget page exists. */}}
   {{ if not $headless_bundle }}
-    {{ errorf "Widget Page not found at %s!" $page }}
-    {{ errorf "View the Widget Page documentation at https://wowchemy.com/docs/managing-content/#create-a-widget-page ." }}
-    {{ errorf "If the Hugo version is between 0.65 and 0.68, it may be a confirmed Hugo bug that is expected to be fixed in Hugo v0.69: https://github.com/wowchemy/wowchemy-hugo-modules/issues/1595#issuecomment-605514973 ." }}
+    {{ warnf "Widget Page not found at %s!" $page }}
+    {{ warnf "View the Widget Page documentation at https://wowchemy.com/docs/managing-content/#create-a-widget-page ." }}
+    {{ warnf "If the Hugo version is between 0.65 and 0.68, it may be a confirmed Hugo bug that is expected to be fixed in Hugo v0.69: https://github.com/wowchemy/wowchemy-hugo-modules/issues/1595#issuecomment-605514973 ." }}
   {{ end }}
 {{ end }}
 
@@ -28,13 +28,14 @@
   {{/* Begin widget styling */}}
   {{ $bg := $st.Params.design.background }}
   {{ $style := "" }}
+  {{ $style_bg := "" }}
 
   {{ if $bg.color }}
-    {{ $style = printf "background-color: %s;" ($bg.color | default "transparent") }}
+    {{ $style_bg = printf "background-color: %s;" ($bg.color | default "transparent") }}
   {{ end }}
 
   {{ if and $bg.gradient_start $bg.gradient_end }}
-    {{ $style = printf "%sbackground-image: linear-gradient(%s, %s);" $style $bg.gradient_start $bg.gradient_end }}
+    {{ $style_bg = printf "%sbackground-image: linear-gradient(%s, %s);" $style_bg $bg.gradient_start $bg.gradient_end }}
   {{ end }}
 
   {{ if $bg.image }}
@@ -44,14 +45,14 @@
     {{ end }}
     {{/* See Hugo note on linking assets in styles: https://github.com/gohugoio/hugoThemes#common-permalink-issues */}}
     {{ $media_dir := $.Scratch.Get "media_dir" }}
-    {{ $style = printf "%sbackground-image: %s url('%s');" $style $darken (printf "%s/%s" $media_dir $bg.image | absURL) }}
+    {{ $style_bg = printf "%sbackground-image: %s url('%s');" $style_bg $darken (printf "%s/%s" $media_dir $bg.image | absURL) }}
     {{ with $bg.image_size }}
       {{/* Allow sizes: actual, cover, and contain. */}}
-      {{ $style = printf "%sbackground-size: %s;" $style (replace . "actual" "auto") }}
+      {{ $style_bg = printf "%sbackground-size: %s;" $style_bg (replace . "actual" "auto") }}
     {{ end }}
     {{ with $bg.image_position }}
       {{/* Allow valid CSS positions including left, center, and right. */}}
-      {{ $style = printf "%sbackground-position: %s;" $style . }}
+      {{ $style_bg = printf "%sbackground-position: %s;" $style_bg . }}
     {{ end }}
   {{ end }}
 
@@ -98,9 +99,37 @@
     {{ $use_container = $cfg.use_container }}
   {{end}}
 
-  <section id="{{$hash_id}}" class="home-section {{$widget_class}} {{if $bg.text_color_light}}dark{{end}} {{if $bg.image}} bg-image{{if ($bg.image_parallax | default true) }} parallax{{end}}{{end}} {{with $css_classes}}{{.}}{{end}}" {{with $style}}style="{{. | safeCSS}}"{{end}} {{print $extra_attributes | safeHTMLAttr}}>
+  {{ $columns := $st.Params.design.columns | default "2" }}
+  {{ $use_cols := in (slice "pages" "featured" "experience" "accomplishments" "contact" "blank" "tag_cloud" "portfolio") $widget }}
+
+  {{/* Dedicated child div for bg prevents parallax 100% height issue within new CSS grid page wrapper. */}}
+  <section id="{{$hash_id}}" class="home-section {{$widget_class}} {{if $bg.text_color_light}}dark{{end}} {{with $css_classes}}{{.}}{{end}}" {{with $style}}style="{{. | safeCSS}}"{{end}} {{print $extra_attributes | safeHTMLAttr}}>
+   <div class="home-section-bg {{if $bg.image}} bg-image{{if ($bg.image_parallax | default true) }} parallax{{end}}{{end}}" {{with $style_bg}}style="{{. | safeCSS}}"{{end}}></div>
     {{if $use_container}}<div class="container">{{end}}
+
+    {{if $use_cols}}
+      <div class="row  {{if not $st.Title | or (eq $columns "1") }}justify-content-center{{end}}">
+      {{ if $st.Title }}
+        {{ if eq $columns "1" }}
+          <div class="col-12 section-heading text-center">
+            {{ with $st.Title }}<h1>{{ . | markdownify | emojify }}</h1>{{ end }}
+            {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
+          </div>
+        {{else}}
+          <div class="col-12 col-lg-4 section-heading">
+            {{ with $st.Title }}<h1>{{ . | markdownify | emojify }}</h1>{{ end }}
+            {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
+          </div>
+        {{end}}
+      {{end}}
+    {{end}}
+
       {{ partial $widget_path $widget_args }}
+
+    {{if $use_cols}}
+      </div>
+    {{end}}
+
     {{if $use_container}}</div>{{end}}
   </section>
 {{ end }}

+ 4 - 2
wowchemy/layouts/partials/widgets/about.html

@@ -58,7 +58,7 @@
           {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
         {{ end }}
         <li>
-          <a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}>
+          <a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }} aria-label="{{ .icon }}">
             <i class="{{ $pack }} {{ $pack_prefix }}-{{ .icon }} big-icon"></i>
           </a>
         </li>
@@ -72,7 +72,9 @@
     {{/* Only display widget title in explicit instances of about widget, not in author pages. */}}
     {{ if and $page.Params.widget $page.Title }}<h1>{{ $page.Title | markdownify | emojify }}</h1>{{ end }}
 
-    {{ $person_page.Content }}
+    <div class="article-style">
+      {{ $person_page.Content }}
+    </div>
 
     <div class="row">
 

+ 31 - 36
wowchemy/layouts/partials/widgets/accomplishments.html

@@ -1,48 +1,43 @@
 {{ $ := .root }}
 {{ $page := .page }}
+{{ $columns := $page.Params.design.columns | default "2" }}
 
 <!-- Accomplishments widget -->
-<div class="row">
-  <div class="col-12 col-lg-4 section-heading">
-    <h1>{{ with $page.Title }}{{ . | markdownify }}{{ end }}</h1>
-    {{ with $page.Params.subtitle }}<p>{{ . | markdownify }}</p>{{ end }}
-  </div>
-  <div class="col-12 col-lg-8">
-    {{ with $page.Content }}{{ . }}{{ end }}
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
+  {{ with $page.Content }}{{ . }}{{ end }}
 
-    {{ if $page.Params.item }}
-    {{ range $idx, $key := sort $page.Params.item ".date_start" "desc" }}
-      <div class="card experience course">
-        <div class="card-body">
-          {{- with .url -}}<a href="{{.}}" target="_blank" rel="noopener">{{- end -}}
-          <h4 class="card-title exp-title text-muted my-0">{{.title | markdownify | emojify}}</h4>
-          {{- with .url -}}</a>{{- end -}}
+  {{ if $page.Params.item }}
+  {{ range $idx, $key := sort $page.Params.item ".date_start" "desc" }}
+    <div class="card experience course">
+      <div class="card-body">
+        {{- with .url -}}<a href="{{.}}" target="_blank" rel="noopener">{{- end -}}
+        <h4 class="card-title exp-title text-muted my-0">{{.title | markdownify | emojify}}</h4>
+        {{- with .url -}}</a>{{- end -}}
 
-          <div class="card-subtitle my-0 article-metadata">
-            {{- with .organization_url}}<a href="{{.}}" target="_blank" rel="noopener">{{end -}}
-            {{- .organization | markdownify | emojify -}}
-            {{- with .organization_url}}</a>{{end -}}
+        <div class="card-subtitle my-0 article-metadata">
+          {{- with .organization_url}}<a href="{{.}}" target="_blank" rel="noopener">{{end -}}
+          {{- .organization | markdownify | emojify -}}
+          {{- with .organization_url}}</a>{{end -}}
 
-            <span class="middot-divider"></span>
+          <span class="middot-divider"></span>
 
-            {{ (time .date_start).Format ($page.Params.date_format | default "Jan 2006") }}
-            {{ if .date_end}}
-            – {{ (time .date_end).Format ($page.Params.date_format | default "Jan 2006") }}
-            {{end}}
-          </div>
-
-          {{with .description}}
-            <div class="card-text">{{. | markdownify | emojify}}</div>
+          {{ (time .date_start).Format ($page.Params.date_format | default "Jan 2006") }}
+          {{ if .date_end}}
+          – {{ (time .date_end).Format ($page.Params.date_format | default "Jan 2006") }}
           {{end}}
-
-          {{ with .certificate_url }}
-            <a class="card-link" href="{{.}}" target="_blank" rel="noopener">
-              {{ i18n "see_certificate" | default "See certificate" }}
-            </a>
-          {{ end }}
         </div>
+
+        {{with .description}}
+          <div class="card-text">{{. | markdownify | emojify}}</div>
+        {{end}}
+
+        {{ with .certificate_url }}
+          <a class="card-link" href="{{.}}" target="_blank" rel="noopener">
+            {{ i18n "see_certificate" | default "See certificate" }}
+          </a>
+        {{ end }}
       </div>
-    {{end}}
-    {{end}}
-  </div>
+    </div>
+  {{end}}
+  {{end}}
 </div>

+ 9 - 17
wowchemy/layouts/partials/widgets/blank.html

@@ -1,20 +1,12 @@
 {{ $st := .page }}
 {{ $columns := $st.Params.design.columns | default "2" }}
 
-<div class="row">
-  {{ if ne $columns "1" }}
-    <div class="col-12 col-lg-4 section-heading">
-      {{ with $st.Title }}<h1>{{ . | markdownify | emojify }}</h1>{{ end }}
-      {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-    </div>
-    <div class="col-12 col-lg-8">
-      {{ $st.Content }}
-    </div>
-  {{ else }}
-    <div class="col-lg-12">
-      {{ with $st.Title }}<h1>{{ . | markdownify | emojify }}</h1>{{ end }}
-      {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-      {{ $st.Content }}
-    </div>
-  {{ end }}
-</div>
+{{ if ne $columns "1" }}
+  <div class="col-12 col-lg-8">
+    {{ $st.Content }}
+  </div>
+{{ else }}
+  <div class="col-12">
+    {{ $st.Content }}
+  </div>
+{{ end }}

+ 113 - 118
wowchemy/layouts/partials/widgets/contact.html

@@ -1,41 +1,37 @@
 {{ $ := .root }}
 {{ $st := .page }}
-{{ $autolink := default true $st.Params.autolink }}
+{{ $autolink := default true $st.Params.content.autolink }}
 {{ $data := site.Params }}
 
+{{ $form_provider := lower $st.Params.content.form.provider | default "" }}
+{{ $form_provider_legacy := $st.Params.email_form | default 0 }}
+
+{{ $use_netlify_form := eq $form_provider "netlify" | or (eq $form_provider_legacy 1) }}
+{{ $use_formspree_form := eq $form_provider "formspree" | or (eq $form_provider_legacy 2) }}
+{{ $use_form := or $use_netlify_form $use_formspree_form }}
+
+{{ $use_netlify_captcha := $st.Params.content.form.netlify.captcha | default true }}
+
 {{ $columns := $st.Params.design.columns | default "2" }}
 
-<div class="row contact-widget {{if not $st.Title | or (eq $columns "1") }}justify-content-center{{end}}">
-  {{ if $st.Title }}
-    {{ if eq $columns "1" }}
-      <div class="col-12 col-lg-8 section-heading text-center">
-        {{ with $st.Title }}<h1>{{.}}</h1>{{ end }}
-        {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-      </div>
-    {{else}}
-      <div class="col-12 col-lg-4 section-heading">
-        {{ with $st.Title }}<h1>{{.}}</h1>{{ end }}
-        {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-      </div>
-    {{end}}
-  {{ end }}
-  <div class="col-12 col-lg-8">
-    {{ with $st.Content }}{{ . }}{{ end }}
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
+  {{ with $st.Content }}{{ . }}{{ end }}
 
-    {{ if $st.Params.email_form }}
+  {{ if $use_form }}
 
     {{ $post_action := "" }}
-    {{ if eq $st.Params.email_form 1 }}
+    {{ if $use_netlify_form }}
       {{ $post_action = "netlify" }}
-    {{ else }}
-      {{ if not $data.email }}
-        {{ errorf "Please set an email address for the contact form using the `email` parameter in `params.toml`. Otherwise, set `email_form = 0` to disable the contact form." }}
+    {{ else if $use_formspree_form }}
+      {{ if not $st.Params.content.form.formspree.id }}
+        {{ errorf "You have chosen to use Formspree as the provider for the contact form. Please set your Formspree Form ID in the Contact widget or disable the form." }}
+        {{ errorf "Documentation: https://wowchemy.com/docs/page-builder/#contact" }}
       {{ end }}
-      {{ $post_action = printf "action=\"https://formspree.io/%s\"" $data.email }}
+      {{ $post_action = printf "action=\"https://formspree.io/f/%s\"" $st.Params.content.form.formspree.id }}
     {{end}}
 
     <div class="mb-3">
-      <form name="contact" method="POST" {{ $post_action | safeHTMLAttr }} {{ if eq $st.Params.email_form 1 }}netlify-honeypot="welcome-bot"{{end}} {{ if $st.Params.netlify.captcha }}data-netlify-recaptcha="true"{{end}}>
+      <form name="contact" method="POST" {{ $post_action | safeHTMLAttr }} {{ if $use_netlify_form }}netlify-honeypot="welcome-bot"{{end}} {{ if $use_netlify_captcha }}data-netlify-recaptcha="true"{{end}}>
         <div class="form-group form-inline">
           <label class="sr-only" for="inputName">{{ i18n "contact_name" }}</label>
           <input type="text" name="name" class="form-control w-100" id="inputName" placeholder="{{ i18n "contact_name" | default "Name" }}" required>
@@ -48,116 +44,115 @@
           <label class="sr-only" for="inputMessage">{{ i18n "contact_message" }}</label>
           <textarea name="message" class="form-control" id="inputMessage" rows="5" placeholder="{{ i18n "contact_message" | default "Message" }}" required></textarea>
         </div>
-        {{ if eq $st.Params.email_form 1 }}
-        <div class="d-none">
-          <label>Do not fill this field unless you are a bot: <input name="welcome-bot"></label>
-        </div>
-        {{ end }}
-        {{ if $st.Params.netlify.captcha }}
-        <div class="form-group" data-netlify-recaptcha="true"></div>
+        {{ if $use_netlify_form }}
+          <div class="d-none">
+            <label>Do not fill this field unless you are a bot: <input name="welcome-bot"></label>
+          </div>
+          {{ if $use_netlify_captcha }}
+            <div class="form-group" data-netlify-recaptcha="true"></div>
+          {{ end }}
         {{ end }}
         <button type="submit" class="btn btn-outline-primary px-3 py-2">{{ i18n "contact_send" | default "Send" }}</button>
       </form>
     </div>
-    {{end}}
-
-    <ul class="fa-ul">
+  {{end}}
 
-      {{ if and $data.email (not $st.Params.email_form) }}
-      <li>
-        <i class="fa-li fas fa-envelope fa-2x" aria-hidden="true"></i>
-        <span id="person-email">
-        {{- if $autolink }}<a href="mailto:{{ $data.email }}">{{ $data.email }}</a>{{ else }}{{ $data.email }}{{ end -}}
-        </span>
-      </li>
-      {{ end }}
+  <ul class="fa-ul">
 
-      {{ with $data.gnupg_key }}
-      <li>
-        <i class="fa-li fa fa-key fa-2x" aria-hidden="true"></i>
-        <code>gpg --recv-keys {{ . }}</code>
-      </li>
-      {{ end }}
-
-      {{ with $data.phone }}
-      <li>
-        <i class="fa-li fas fa-phone fa-2x" aria-hidden="true"></i>
-        <span id="person-telephone">
-        {{- if $autolink }}<a href="tel:{{ . }}">{{ . }}</a>{{ else }}{{ . }}{{ end -}}
-        </span>
-      </li>
-      {{ end }}
+    {{ with $data.gnupg_key }}
+    <li>
+      <i class="fa-li fa fa-key fa-2x" aria-hidden="true"></i>
+      <code>gpg --recv-keys {{ . }}</code>
+    </li>
+    {{ end }}
 
-      {{ $addr_formatted := "" }}{{/* Scoping for maps. */}}
-      {{ if $data.address.street | or $data.address.city | or $data.address.region | or $data.address.postcode | or $data.address.country }}
-        {{ $addr_formatted = partial "functions/get_address" (dict "root" . "address" $data.address) }}
-        <li>
-          <i class="fa-li fas fa-map-marker fa-2x" aria-hidden="true"></i>
-          <span id="person-address">{{$addr_formatted}}</span>
-        </li>
-      {{ end }}
+    {{ if $data.email }}
+    <li>
+      <i class="fa-li fas fa-envelope fa-2x" aria-hidden="true"></i>
+      <span id="person-email">
+      {{- if $autolink }}<a href="mailto:{{ $data.email }}">{{ $data.email }}</a>{{ else }}{{ $data.email }}{{ end -}}
+      </span>
+    </li>
+    {{ end }}
 
-      {{ with $data.directions }}
-      <li>
-        <i class="fa-li fas fa-compass fa-2x" aria-hidden="true"></i>
-        <span>{{ . | markdownify | emojify }}</span>
-      </li>
-      {{ end }}
+    {{ with $data.phone }}
+    <li>
+      <i class="fa-li fas fa-phone fa-2x" aria-hidden="true"></i>
+      <span id="person-telephone">
+      {{- if $autolink }}<a href="tel:{{ . }}">{{ . }}</a>{{ else }}{{ . }}{{ end -}}
+      </span>
+    </li>
+    {{ end }}
 
-      {{ with $data.office_hours }}
+    {{ $addr_formatted := "" }}{{/* Scoping for maps. */}}
+    {{ if $data.address.street | or $data.address.city | or $data.address.region | or $data.address.postcode | or $data.address.country }}
+      {{ $addr_formatted = partial "functions/get_address" (dict "root" . "address" $data.address) }}
       <li>
-        <i class="fa-li fas fa-clock fa-2x" aria-hidden="true"></i>
-        <span>
-          {{- if not (reflect.IsSlice .)}}{{/* Support legacy string format. */}}
-            {{- . | markdownify | emojify -}}
-          {{else}}
-            {{- delimit . "<br>" | markdownify | emojify -}}
-          {{end -}}
-        </span>
+        <i class="fa-li fas fa-map-marker fa-2x" aria-hidden="true"></i>
+        <span id="person-address">{{$addr_formatted}}</span>
       </li>
-      {{ end }}
+    {{ end }}
 
-      {{ with $data.appointment_url }}
-      <li>
-        <i class="fa-li fas fa-calendar-check fa-2x" aria-hidden="true"></i>
-        <a href="{{ . }}" target="_blank" rel="noopener">{{ i18n "book_appointment" | default "Book an appointment" }}</a>
-      </li>
-      {{ end }}
+    {{ with $data.directions }}
+    <li>
+      <i class="fa-li fas fa-compass fa-2x" aria-hidden="true"></i>
+      <span>{{ . | markdownify | emojify }}</span>
+    </li>
+    {{ end }}
 
-      {{/* Contact links. */}}
-      {{ range $data.contact_links }}
-      {{ $pack := or .icon_pack "fas" }}
-      {{ $pack_prefix := $pack }}
-      {{ if in (slice "fab" "fas" "far" "fal") $pack }}
-        {{ $pack_prefix = "fa" }}
-      {{ end }}
-      {{ $link := .link }}
-      {{ $scheme := (urls.Parse $link).Scheme }}
-      {{ $target := "" }}
-      {{ if not $scheme }}
-        {{ $link = .link | relLangURL }}
-      {{ else if in (slice "http" "https") $scheme }}
-        {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
-      {{ end }}
-      <li>
-        <i class="fa-li {{ $pack }} {{ $pack_prefix }}-{{ .icon }} fa-2x" aria-hidden="true"></i>
-        <a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}>{{.name|markdownify|emojify|safeHTML}}</a>
-      </li>
-      {{ end }}
+    {{ with $data.office_hours }}
+    <li>
+      <i class="fa-li fas fa-clock fa-2x" aria-hidden="true"></i>
+      <span>
+        {{- if not (reflect.IsSlice .)}}{{/* Support legacy string format. */}}
+          {{- . | markdownify | emojify -}}
+        {{else}}
+          {{- delimit . "<br>" | markdownify | emojify -}}
+        {{end -}}
+      </span>
+    </li>
+    {{ end }}
 
-    </ul>
+    {{ with $data.appointment_url }}
+    <li>
+      <i class="fa-li fas fa-calendar-check fa-2x" aria-hidden="true"></i>
+      <a href="{{ . }}" target="_blank" rel="noopener">{{ i18n "book_appointment" | default "Book an appointment" }}</a>
+    </li>
+    {{ end }}
 
-    {{ if and site.Params.map.engine $data.coordinates.latitude }}
-    <div class="d-none">
-      <input id="map-provider" value="{{ site.Params.map.engine }}">
-      <input id="map-lat" value="{{ $data.coordinates.latitude }}">
-      <input id="map-lng" value="{{ $data.coordinates.longitude }}">
-      <input id="map-dir" value="{{ $addr_formatted }}">
-      <input id="map-zoom" value="{{ site.Params.map.zoom | default "15" }}">
-      <input id="map-api-key" value="{{ site.Params.map.api_key }}">
-    </div>
-    <div id="map"></div>
+    {{/* Contact links. */}}
+    {{ range $data.contact_links }}
+    {{ $pack := or .icon_pack "fas" }}
+    {{ $pack_prefix := $pack }}
+    {{ if in (slice "fab" "fas" "far" "fal") $pack }}
+      {{ $pack_prefix = "fa" }}
     {{ end }}
+    {{ $link := .link }}
+    {{ $scheme := (urls.Parse $link).Scheme }}
+    {{ $target := "" }}
+    {{ if not $scheme }}
+      {{ $link = .link | relLangURL }}
+    {{ else if in (slice "http" "https") $scheme }}
+      {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
+    {{ end }}
+    <li>
+      <i class="fa-li {{ $pack }} {{ $pack_prefix }}-{{ .icon }} fa-2x" aria-hidden="true"></i>
+      <a href="{{ $link | safeURL }}" {{ $target | safeHTMLAttr }}>{{.name|markdownify|emojify|safeHTML}}</a>
+    </li>
+    {{ end }}
+
+  </ul>
 
+  {{ if and site.Params.map.engine $data.coordinates.latitude }}
+  <div class="d-none">
+    <input id="map-provider" value="{{ site.Params.map.engine }}">
+    <input id="map-lat" value="{{ $data.coordinates.latitude }}">
+    <input id="map-lng" value="{{ $data.coordinates.longitude }}">
+    <input id="map-dir" value="{{ $addr_formatted }}">
+    <input id="map-zoom" value="{{ site.Params.map.zoom | default "15" }}">
+    <input id="map-api-key" value="{{ site.Params.map.api_key }}">
   </div>
+  <div id="map"></div>
+  {{ end }}
+
 </div>

+ 42 - 47
wowchemy/layouts/partials/widgets/experience.html

@@ -1,59 +1,54 @@
 {{ $ := .root }}
 {{ $page := .page }}
+{{ $columns := $page.Params.design.columns | default "2" }}
 
 <!-- Experience widget -->
-<div class="row">
-  <div class="col-12 col-lg-4 section-heading">
-    <h1>{{ with $page.Title }}{{ . | markdownify }}{{ end }}</h1>
-    {{ with $page.Params.subtitle }}<p>{{ . | markdownify }}</p>{{ end }}
-  </div>
-  <div class="col-12 col-lg-8">
-    {{ with $page.Content }}{{ . }}{{ end }}
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
+  {{ with $page.Content }}{{ . }}{{ end }}
 
-    {{ if $page.Params.experience }}
-    {{ $exp_len := len $page.Params.experience }}
-    {{ range $idx, $key := sort $page.Params.experience ".date_start" "desc" }}
-    <div class="row experience">
-      <!-- Timeline -->
-      <div class="col-auto text-center flex-column d-none d-sm-flex">
-        <div class="row h-50">
-          <div class="col {{if gt $idx 0}}border-right{{end}}">&nbsp;</div>
-          <div class="col">&nbsp;</div>
-        </div>
-        <div class="m-2">
-          <span class="badge badge-pill border {{if not .date_end}}exp-fill{{end}}">&nbsp;</span>
-        </div>
-        <div class="row h-50">
-          <div class="col {{if lt $idx (sub $exp_len 1)}}border-right{{end}}">&nbsp;</div>
-          <div class="col">&nbsp;</div>
-        </div>
+  {{ if $page.Params.experience }}
+  {{ $exp_len := len $page.Params.experience }}
+  {{ range $idx, $key := sort $page.Params.experience ".date_start" "desc" }}
+  <div class="row experience">
+    <!-- Timeline -->
+    <div class="col-auto text-center flex-column d-none d-sm-flex">
+      <div class="row h-50">
+        <div class="col {{if gt $idx 0}}border-right{{end}}">&nbsp;</div>
+        <div class="col">&nbsp;</div>
       </div>
-      <!-- Content -->
-      <div class="col py-2">
-        <div class="card">
-          <div class="card-body">
-            <h4 class="card-title exp-title text-muted mt-0 mb-1">{{.title | markdownify | emojify}}</h4>
-            <h4 class="card-title exp-company text-muted my-0">
-              {{- with .company_url}}<a href="{{.}}" target="_blank" rel="noopener">{{end}}{{.company | markdownify | emojify}}{{with .company_url}}</a>{{end -}}
-            </h4>
-            <div class="text-muted exp-meta">
-              {{ (time .date_start).Format ($page.Params.date_format | default "January 2006") }} –
-              {{ if .date_end}}
-                {{ (time .date_end).Format ($page.Params.date_format | default "January 2006") }}
-              {{else}}
-                {{ i18n "present" | default "Present" }}
-              {{end}}
-              {{with .location}}
-                <span class="middot-divider"></span>
-                <span>{{.}}</span>
-              {{end}}
-            </div>
-            {{with .description}}<div class="card-text">{{. | markdownify | emojify}}</div>{{end}}
+      <div class="m-2">
+        <span class="badge badge-pill border {{if not .date_end}}exp-fill{{end}}">&nbsp;</span>
+      </div>
+      <div class="row h-50">
+        <div class="col {{if lt $idx (sub $exp_len 1)}}border-right{{end}}">&nbsp;</div>
+        <div class="col">&nbsp;</div>
+      </div>
+    </div>
+    <!-- Content -->
+    <div class="col py-2">
+      <div class="card">
+        <div class="card-body">
+          <h4 class="card-title exp-title text-muted mt-0 mb-1">{{.title | markdownify | emojify}}</h4>
+          <h4 class="card-title exp-company text-muted my-0">
+            {{- with .company_url}}<a href="{{.}}" target="_blank" rel="noopener">{{end}}{{.company | markdownify | emojify}}{{with .company_url}}</a>{{end -}}
+          </h4>
+          <div class="text-muted exp-meta">
+            {{ (time .date_start).Format ($page.Params.date_format | default "January 2006") }} –
+            {{ if .date_end}}
+              {{ (time .date_end).Format ($page.Params.date_format | default "January 2006") }}
+            {{else}}
+              {{ i18n "present" | default "Present" }}
+            {{end}}
+            {{with .location}}
+              <span class="middot-divider"></span>
+              <span>{{.}}</span>
+            {{end}}
           </div>
+          {{with .description}}<div class="card-text">{{. | markdownify | emojify}}</div>{{end}}
         </div>
       </div>
     </div>
-    {{end}}
-    {{end}}
   </div>
+  {{end}}
+  {{end}}
 </div>

+ 36 - 26
wowchemy/layouts/partials/widgets/featured.html

@@ -29,6 +29,8 @@
   {{ $query = $query | intersect $archive_page.Pages }}
 {{ end }}
 
+{{ $count := len $query }}
+
 {{/* Sort */}}
 {{ $sort_by := "Date" }}
 {{ $query = sort $query $sort_by $items_sort }}
@@ -40,7 +42,7 @@
 {{ $i18n := "" }}
 {{ if eq $items_type "post" }}
   {{ $i18n = "more_posts" }}
-{{ else if eq $items_type "talk" }}
+{{ else if eq $items_type "event" }}
   {{ $i18n = "more_talks" }}
 {{ else if eq $items_type "publication" }}
   {{ $i18n = "more_publications" }}
@@ -48,35 +50,43 @@
   {{ $i18n = "more_pages" }}
 {{ end }}
 
-<div class="row">
-  <div class="col-12 col-lg-4 section-heading">
-    <h1>{{ with $st.Title }}{{ . | markdownify | emojify }}{{ end }}</h1>
-    {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-  </div>
-  <div class="col-12 col-lg-8">
-
-    {{ with $st.Content }}{{ . }}{{ end }}
-
-    {{ range $post := $query }}
-      {{ if eq $st.Params.design.view 1 }}
-        {{ partial "li_list" . }}
-      {{ else if eq $st.Params.design.view 3 }}
-        {{ partial "li_card" . }}
-      {{ else if eq $st.Params.design.view 4 | and (eq $items_type "publication") }}
-        {{ partial "li_citation" . }}
-      {{ else }}
-        {{ partial "li_compact" . }}
-      {{ end }}
-    {{end}}
-
-  {{ if $st.Params.content.link_to_archive }}
+{{ $columns := $st.Params.design.columns | default "2" }}
+
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
+
+  {{ with $st.Content }}{{ . }}{{ end }}
+
+  {{ range $post := $query }}
+    {{ if eq $st.Params.design.view 1 }}
+      {{ partial "li_list" . }}
+    {{ else if eq $st.Params.design.view 3 }}
+      {{ partial "li_card" . }}
+    {{ else if eq $st.Params.design.view 4 | and (eq $items_type "publication") }}
+      {{ partial "li_citation" . }}
+    {{ else }}
+      {{ partial "li_compact" . }}
+    {{ end }}
+  {{end}}
+
+  {{/* Archive link */}}
+  {{ $show_archive_link := $st.Params.content.archive.enable | default (gt $count $items_count) }}
+  {{ if $show_archive_link }}
+
+    {{ $archive_link := "" }}
+    {{ if $st.Params.content.archive.link }}
+      {{ $archive_link = $st.Params.content.archive.link | relLangURL }}
+    {{ else }}
+      {{ $archive_link = $archive_page.RelPermalink }}
+    {{ end }}
+
+    {{ $archive_text := $st.Params.content.archive.text | default (i18n $i18n) | default "See all" }}
+
     <div class="see-all">
-      <a href="{{ $archive_page.RelPermalink }}">
-        {{ i18n $i18n | default "See all" }}
+      <a href="{{ $archive_link }}">
+        {{ $archive_text | emojify }}
         <i class="fas fa-angle-right"></i>
       </a>
     </div>
   {{ end }}
 
-  </div>
 </div>

+ 2 - 2
wowchemy/layouts/partials/widgets/hero.html

@@ -4,7 +4,7 @@
 
 {{ if $page.Params.hero_media }}
 <div class="row">
-  <div class="col-md-6 order-md-1 text-center text-md-left">
+  <div class="col-12 col-md-6 order-md-1 text-center text-md-left">
 {{ end }}
 
     <h1 class="hero-title">
@@ -59,7 +59,7 @@
   {{ if $page.Params.hero_media }}
     {{ $media_dir := $.Scratch.Get "media_dir" }}
   </div>
-  <div class="col-6 mx-auto col-md-6 order-md-2 hero-media">
+  <div class="col-12 mx-auto col-md-6 order-md-2 hero-media">
     <img src="{{ printf "%s/%s" $media_dir $page.Params.hero_media | relURL }}" alt="">
   </div>
 </div>

+ 36 - 51
wowchemy/layouts/partials/widgets/pages.html

@@ -61,7 +61,7 @@
 {{ $i18n := "" }}
 {{ if eq $items_type "post" }}
   {{ $i18n = "more_posts" }}
-{{ else if eq $items_type "talk" }}
+{{ else if eq $items_type "event" }}
   {{ $i18n = "more_talks" }}
 {{ else if eq $items_type "publication" }}
   {{ $i18n = "more_publications" }}
@@ -71,56 +71,41 @@
 
 {{ $columns := $st.Params.design.columns | default "2" }}
 
-<div class="row {{if not $st.Title | or (eq $columns "1") }}justify-content-center{{end}}">
-  {{ if $st.Title }}
-    {{ if eq $columns "1" }}
-      <div class="col-12 col-lg-8 section-heading text-center">
-        {{ with $st.Title }}<h1>{{.}}</h1>{{ end }}
-        {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-      </div>
-    {{else}}
-      <div class="col-12 col-lg-4 section-heading">
-        {{ with $st.Title }}<h1>{{.}}</h1>{{ end }}
-        {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-      </div>
-    {{end}}
-  {{ end }}
-  <div class="col-12 col-lg-8">
-
-    {{ with $st.Content }}{{ . }}{{ end }}
-
-    {{ range $post := $query }}
-      {{ if eq $st.Params.design.view 1 }}
-        {{ partial "li_list" . }}
-      {{ else if eq $st.Params.design.view 3 }}
-        {{ partial "li_card" . }}
-      {{ else if eq $st.Params.design.view 4 | and (eq $items_type "publication") }}
-        {{ partial "li_citation" . }}
-      {{ else }}
-        {{ partial "li_compact" . }}
-      {{ end }}
-    {{end}}
-
-    {{/* Archive link */}}
-    {{ $show_archive_link := $st.Params.content.archive.enable | default (gt $count $items_count) }}
-    {{ if $show_archive_link }}
-
-      {{ $archive_link := "" }}
-      {{ if $st.Params.content.archive.link }}
-        {{ $archive_link = $st.Params.content.archive.link | relLangURL }}
-      {{ else }}
-        {{ $archive_link = $archive_page.RelPermalink }}
-      {{ end }}
-
-      {{ $archive_text := $st.Params.content.archive.text | default (i18n $i18n) | default "See all" }}
-
-      <div class="see-all">
-        <a href="{{ $archive_link }}">
-          {{ $archive_text | emojify }}
-          <i class="fas fa-angle-right"></i>
-        </a>
-      </div>
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
+
+  {{ with $st.Content }}{{ . }}{{ end }}
+
+  {{ range $post := $query }}
+    {{ if eq $st.Params.design.view 1 }}
+      {{ partial "li_list" . }}
+    {{ else if eq $st.Params.design.view 3 }}
+      {{ partial "li_card" . }}
+    {{ else if eq $st.Params.design.view 4 | and (eq $items_type "publication") }}
+      {{ partial "li_citation" . }}
+    {{ else }}
+      {{ partial "li_compact" . }}
     {{ end }}
+  {{end}}
+
+  {{/* Archive link */}}
+  {{ $show_archive_link := $st.Params.content.archive.enable | default (gt $count $items_count) }}
+  {{ if $show_archive_link }}
+
+    {{ $archive_link := "" }}
+    {{ if $st.Params.content.archive.link }}
+      {{ $archive_link = $st.Params.content.archive.link | relLangURL }}
+    {{ else }}
+      {{ $archive_link = $archive_page.RelPermalink }}
+    {{ end }}
+
+    {{ $archive_text := $st.Params.content.archive.text | default (i18n $i18n) | default "See all" }}
+
+    <div class="see-all">
+      <a href="{{ $archive_link }}">
+        {{ $archive_text | emojify }}
+        <i class="fas fa-angle-right"></i>
+      </a>
+    </div>
+  {{ end }}
 
-  </div>
 </div>

+ 4 - 1
wowchemy/layouts/partials/widgets/people.html

@@ -5,6 +5,8 @@
 {{ $page := .page }}
 {{ $show_social := $page.Params.design.show_social | default false }}
 {{ $show_interests := $page.Params.design.show_interests | default true }}
+{{ $show_organizations := $page.Params.design.show_organizations | default false }}
+{{ $show_role := $page.Params.design.show_role | default true }}
 
 <div class="row justify-content-center people-widget">
   {{ with $page.Title }}
@@ -51,7 +53,8 @@
 
     <div class="portrait-title">
       <h2>{{with $link}}<a href="{{.}}">{{end}}{{ .Title }}{{if $link}}</a>{{end}}</h2>
-      {{ with .Params.role }}<h3>{{ . | markdownify | emojify }}</h3>{{ end }}
+      {{ if and $show_organizations .Params.organizations }}{{ range .Params.organizations }}<h3>{{ .name }}</h3>{{ end }}{{ end }}
+      {{ if and $show_role .Params.role }}<h3>{{ .Params.role | markdownify | emojify }}</h3>{{ end }}
       {{ if $show_social }}{{ partial "social_links" . }}{{ end }}
       {{ if and $show_interests .Params.interests }}<p class="people-interests">{{ delimit .Params.interests ", " | markdownify | emojify }}</p>{{ end }}
     </div>

+ 45 - 67
wowchemy/layouts/partials/widgets/portfolio.html

@@ -6,88 +6,66 @@
 {{ $items_type := $st.Params.content.page_type | default "project" }}
 {{ $columns := $st.Params.design.columns | default "2" }}
 
-{{ if ne $columns "1" }}
-{{/* Standard dual-column layout. */}}
-
-<div class="row">
-  <div class="col-12 col-lg-4 section-heading">
-
-    {{ with $st.Title }}<h1>{{ . | markdownify | emojify }}</h1>{{ end }}
-    {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-
-  </div>
-  <div class="col-12 col-lg-8">
-
-{{ else }}
-{{/* Single column layout. */}}
-
-<div class="margin-auto">
+{{ $columns := $st.Params.design.columns | default "2" }}
 
-  <div class="center-text">
-    {{ with $st.Title }}<h1 class="mt-0">{{ . | markdownify | emojify }}</h1>{{ end }}
-    {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-  </div>
-  <div>
-{{ end }}
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
 
-    {{ with $st.Content }}{{ . }}{{ end }}
+  {{ with $st.Content }}{{ . }}{{ end }}
 
-    {{ if $st.Params.content.filter_button }}
+  {{ if $st.Params.content.filter_button }}
 
-      {{ $filter_default := default (int $st.Params.content.filter_default) 0 }}
+    {{ $filter_default := default (int $st.Params.content.filter_default) 0 }}
 
-      {{/* Parse default filter tag from front matter in the form of either tag name or CSS class name. */}}
-      {{ $default_filter_tag_raw := (index $st.Params.content.filter_button ($filter_default)).tag }}
-      {{ $default_filter_tag := printf ".js-id-%s" (replace $default_filter_tag_raw " " "-") }}
-      {{ if or (eq (substr $default_filter_tag_raw 0 1) "*") (eq (substr $default_filter_tag_raw 0 1) ".") }}
-        {{ $default_filter_tag = $default_filter_tag_raw }}
-      {{ end }}
-
-      <span class="d-none default-project-filter">{{ $default_filter_tag }}</span>
+    {{/* Parse default filter tag from front matter in the form of either tag name or CSS class name. */}}
+    {{ $default_filter_tag_raw := (index $st.Params.content.filter_button ($filter_default)).tag }}
+    {{ $default_filter_tag := printf ".js-id-%s" (replace $default_filter_tag_raw " " "-") }}
+    {{ if or (eq (substr $default_filter_tag_raw 0 1) "*") (eq (substr $default_filter_tag_raw 0 1) ".") }}
+      {{ $default_filter_tag = $default_filter_tag_raw }}
+    {{ end }}
 
-      {{/* Only show filter buttons if there are multiple filters. */}}
-      {{ if gt (len $st.Params.content.filter_button) 1 }}
-      <div class="project-toolbar">
-        <div class="project-filters">
-          <div class="btn-toolbar">
-            <div class="btn-group flex-wrap">
-              {{ range $idx, $item := $st.Params.content.filter_button }}
-                {{/* Parse filter tag from front matter in the form of either tag name or CSS class name. */}}
-                {{ $data_filter := printf ".js-id-%s" (replace .tag " " "-") }}
-                {{ if or (eq (substr .tag 0 1) "*") (eq (substr .tag 0 1) ".") }}
-                  {{ $data_filter = .tag }}
-                {{ end }}
-                <a href="#" data-filter="{{ $data_filter | safeHTMLAttr }}" class="btn btn-primary btn-lg{{ if eq $idx $filter_default }} active{{ end }}">{{ .name }}</a>
+    <span class="d-none default-project-filter">{{ $default_filter_tag }}</span>
+
+    {{/* Only show filter buttons if there are multiple filters. */}}
+    {{ if gt (len $st.Params.content.filter_button) 1 }}
+    <div class="project-toolbar">
+      <div class="project-filters">
+        <div class="btn-toolbar">
+          <div class="btn-group flex-wrap">
+            {{ range $idx, $item := $st.Params.content.filter_button }}
+              {{/* Parse filter tag from front matter in the form of either tag name or CSS class name. */}}
+              {{ $data_filter := printf ".js-id-%s" (replace .tag " " "-") }}
+              {{ if or (eq (substr .tag 0 1) "*") (eq (substr .tag 0 1) ".") }}
+                {{ $data_filter = .tag }}
               {{ end }}
-            </div>
+              <a href="#" data-filter="{{ $data_filter | safeHTMLAttr }}" class="btn btn-primary btn-lg{{ if eq $idx $filter_default }} active{{ end }}">{{ .name }}</a>
+            {{ end }}
           </div>
         </div>
       </div>
-      {{ end }}
+    </div>
     {{ end }}
+  {{ end }}
 
-    <div class="{{ if or $st.Params.content.filter_button (eq $st.Params.design.view 3) }}isotope projects-container{{end}} {{if eq $st.Params.design.view 3}}js-layout-masonry{{else}}row js-layout-row{{end}} {{ if eq $st.Params.design.view 5 }}project-showcase mt-5{{end}}">
-      {{ range $idx, $item := where site.RegularPages "Type" $items_type }}
+  <div class="{{ if or $st.Params.content.filter_button (eq $st.Params.design.view 3) }}isotope projects-container{{end}} {{if eq $st.Params.design.view 3}}js-layout-masonry{{else}}row js-layout-row{{end}} {{ if eq $st.Params.design.view 5 }}project-showcase mt-5{{end}}">
+    {{ range $idx, $item := where site.RegularPages "Type" $items_type }}
 
-        {{ $link := $item.RelPermalink }}
-        {{ $target := "" }}
-        {{ if $item.Params.external_link }}
-          {{ $link = $item.Params.external_link }}
-          {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
-        {{ end }}
-
-        {{ if eq $st.Params.design.view 1 }}
-          {{ partial "portfolio_li_list" (dict "item" $item) }}
-        {{ else if eq $st.Params.design.view 2 }}
-          {{ partial "portfolio_li_compact" (dict "item" $item) }}
-        {{ else if eq $st.Params.design.view 3 }}
-          {{ partial "portfolio_li_card" (dict "widget" $st "index" $idx "item" $item "link" $link "target" $target) }}
-        {{ else }}
-          {{ partial "portfolio_li_showcase" (dict "widget" $st "index" $idx "item" $item "link" $link "target" $target) }}
-        {{ end }}
+      {{ $link := $item.RelPermalink }}
+      {{ $target := "" }}
+      {{ if $item.Params.external_link }}
+        {{ $link = $item.Params.external_link }}
+        {{ $target = "target=\"_blank\" rel=\"noopener\"" }}
+      {{ end }}
 
+      {{ if eq $st.Params.design.view 1 }}
+        {{ partial "portfolio_li_list" (dict "item" $item) }}
+      {{ else if eq $st.Params.design.view 2 }}
+        {{ partial "portfolio_li_compact" (dict "item" $item) }}
+      {{ else if eq $st.Params.design.view 3 }}
+        {{ partial "portfolio_li_card" (dict "widget" $st "index" $idx "item" $item "link" $link "target" $target) }}
+      {{ else }}
+        {{ partial "portfolio_li_showcase" (dict "widget" $st "index" $idx "item" $item "link" $link "target" $target) }}
       {{ end }}
-    </div>
 
+    {{ end }}
   </div>
 </div>

+ 24 - 30
wowchemy/layouts/partials/widgets/tag_cloud.html

@@ -1,5 +1,3 @@
-{{/* Pages Widget */}}
-
 {{/* Initialise */}}
 {{ $ := .root }}
 {{ $st := .page }}
@@ -18,32 +16,28 @@
 {{ $tags := first $items_count (index site.Taxonomies $taxonomy).ByCount }}
 {{ $count := len $tags }}
 
-<div class="row">
-  <div class="col-12 col-lg-4 section-heading">
-    <h1>{{ with $st.Title }}{{ . | markdownify | emojify }}{{ end }}</h1>
-    {{ with $st.Params.subtitle }}<p>{{ . | markdownify | emojify }}</p>{{ end }}
-  </div>
-  <div class="col-12 col-lg-8">
-    {{ with $st.Content }}{{ . }}{{ end }}
-
-    {{ if ne $count 0 }}
-
-      {{ $fontDelta := sub $fontBig $fontSmall }}
-      {{/* Warning: Hugo's `Reverse` function appears to operate in-place, hence the order of performing $max/$min matters. */}}
-      {{ $max := add (len (index $tags 0).Pages) 1 }}
-      {{ $min := len (index ($tags).Reverse 0).Pages }}
-      {{ $delta := sub $max $min }}
-      {{ $fontStep := div $fontDelta $delta }}
-
-      <div class="tag-cloud">
-        {{ range $name, $term := (sort $tags ".Page.Title" "asc") }}
-          {{ $tagCount := len $term.Pages }}
-          {{ $weight := div (sub (math.Log $tagCount) (math.Log $min)) (sub (math.Log $max) (math.Log $min)) }}
-          {{ $fontSize := add $fontSmall (mul (sub $fontBig $fontSmall) $weight) }}
-          <a href="{{ .Page.RelPermalink }}" style="font-size:{{ $fontSize }}rem">{{ .Page.Title }}</a>
-        {{ end }}
-      </div>
-    {{ end }}
-
-  </div>
+{{ $columns := $st.Params.design.columns | default "2" }}
+
+<div class="col-12 {{if eq $columns "2"}}col-lg-8{{end}}">
+  {{ with $st.Content }}{{ . }}{{ end }}
+
+  {{ if ne $count 0 }}
+
+    {{ $fontDelta := sub $fontBig $fontSmall }}
+    {{/* Warning: Hugo's `Reverse` function appears to operate in-place, hence the order of performing $max/$min matters. */}}
+    {{ $max := add (len (index $tags 0).Pages) 1 }}
+    {{ $min := len (index ($tags).Reverse 0).Pages }}
+    {{ $delta := sub $max $min }}
+    {{ $fontStep := div $fontDelta $delta }}
+
+    <div class="tag-cloud">
+      {{ range $name, $term := (sort $tags ".Page.Title" "asc") }}
+        {{ $tagCount := len $term.Pages }}
+        {{ $weight := div (sub (math.Log $tagCount) (math.Log $min)) (sub (math.Log $max) (math.Log $min)) }}
+        {{ $fontSize := add $fontSmall (mul (sub $fontBig $fontSmall) $weight) }}
+        <a href="{{ .Page.RelPermalink }}" style="font-size:{{ $fontSize }}rem">{{ .Page.Title }}</a>
+      {{ end }}
+    </div>
+  {{ end }}
+
 </div>

+ 1 - 1
wowchemy/layouts/project/single.html

@@ -48,7 +48,7 @@
         {{ end }}
       {{ end }}
 
-      {{ $items := where (where site.RegularPages "Type" "talk") ".Params.projects" "intersect" (slice $project) }}
+      {{ $items := where (where site.RegularPages "Type" "event") ".Params.projects" "intersect" (slice $project) }}
       {{ $talks_len := len $items }}
       {{ if ge $talks_len 1 }}
         <h2>{{ (i18n "talks") }}</h2>

+ 15 - 6
wowchemy/layouts/publication/single.html

@@ -1,4 +1,15 @@
 {{- define "main" -}}
+{{ $pub_types := partial "functions/get_pub_types" $ }}
+{{ $pub_type_param := .Params.publication_types | default (slice 0) }}
+
+{{/* Convert string in form `"0"` to int (`0`) */}}
+{{ $pub_type := (int (index $pub_type_param 0)) | default 0 }}
+
+{{/* Validate Pub Type if defined */}}
+{{ if gt $pub_type (sub (len $pub_types) 1) }}
+  {{ warnf "Unknown publication type in %s" .Path }}
+  {{ $pub_type = 0 }}
+{{ end }}
 
 <div class="pub">
 
@@ -11,19 +22,17 @@
     <p class="pub-abstract">{{ .Params.abstract | markdownify }}</p>
     {{ end }}
 
-    {{ if and (.Params.publication_types) (ne (index .Params.publication_types 0) "0") }}
+    {{/* If the type is Uncategorized, hide the type. */}}
+    {{ if ne $pub_type 0 }}
     <div class="row">
       <div class="col-md-1"></div>
       <div class="col-md-10">
         <div class="row">
           <div class="col-12 col-md-3 pub-row-heading">{{ i18n "publication_type" }}</div>
           <div class="col-12 col-md-9">
-            {{ $pub_types := partial "functions/get_pub_types" $ }}
-            {{ range $index, $pubtype := .Params.publication_types }}
-            <a href="{{ (site.GetPage "section" "publication").RelPermalink }}#{{ . | urlize }}">
-              {{ index $pub_types (int .) }}
+            <a href="{{ (site.GetPage "section" "publication").RelPermalink }}#{{ $pub_type | anchorize }}">
+              {{ index $pub_types $pub_type }}
             </a>
-            {{ end }}
           </div>
         </div>
       </div>

+ 1 - 1
wowchemy/layouts/shortcodes/chart.html

@@ -1,6 +1,6 @@
 {{ $json := printf "./%s.json" (.Get "data") }}
 {{ $id := delimit (shuffle (seq 1 9)) "" }}
-<div id="chart-{{$id}}" class="chart pb-3" style="max-width: 100%; margin: auto;"></div>
+<div id="chart-{{$id}}" class="chart"></div>
 <script>
   (function() {
     let a = setInterval( function() {

+ 1 - 1
wowchemy/layouts/shortcodes/figure.html

@@ -47,7 +47,7 @@
 {{ if $caption }}
   {{/* Localize the figure numbering (if enabled). */}}
   {{ $figure := split (i18n "figure" | default "Figure %d:") "%d" }}
-  <figcaption{{ if eq (.Get "numbered") "true" }} data-pre="{{ index $figure 0 }}" data-post="{{ index $figure 1 }}" class="numbered"{{ end }}>
+  <figcaption{{ if eq (.Get "numbered") "true" }} data-pre="{{- trim (index $figure 0) " " -}}&nbsp;" data-post="{{ index $figure 1 }}&nbsp;" class="numbered"{{ end }}>
     {{ $caption | markdownify | emojify }}
   </figcaption>
 {{ end }}

+ 4 - 4
wowchemy/layouts/shortcodes/gallery.html

@@ -28,9 +28,9 @@
         {{ $caption = .caption }}
       {{ end }}
     {{ end }}
-  <a data-fancybox="gallery-{{$album}}" href="{{ .RelPermalink }}" {{ with $caption }}data-caption="{{.|markdownify|emojify|safeHTMLAttr}}"{{ end }}>
-  <img data-src="{{ $image.RelPermalink }}" class="lazyload" alt="" width="{{ $image.Width }}" height="{{ $image.Height }}">{{/* Width & height (or low res src) required for lazy loading. */}}
-  </a>
+    <a data-fancybox="gallery-{{$album}}" href="{{ .RelPermalink }}" {{ with $caption }}data-caption="{{.|markdownify|emojify|safeHTMLAttr}}"{{ end }}>
+      <img data-src="{{ $image.RelPermalink }}" class="lazyload" alt="{{ plainify $caption | default $filename }}" width="{{ $image.Width }}" height="{{ $image.Height }}">{{/* Width & height (or low res src) required for lazy loading. */}}
+    </a>
   {{end}}
 
   {{else}}
@@ -48,7 +48,7 @@
   {{ end }}
   {{/* Don't lazy load image as cannot init image size from non-Hugo asset, resulting in inaccurate anchor scrolling & active link highlighting. */}}
   <a data-fancybox="gallery{{ with .album }}-{{.}}{{ end }}" {{ with .caption }}data-caption="{{.|markdownify|emojify|safeHTMLAttr}}"{{ end }} href="{{$.Scratch.Get "src"}}">
-    <img src="{{$.Scratch.Get "src"}}" alt="">
+    <img src="{{$.Scratch.Get "src"}}" alt="{{ plainify .caption | default .image }}">
   </a>
   {{end}}
   {{else}}

+ 5 - 12
wowchemy/layouts/shortcodes/spoiler.html

@@ -1,13 +1,6 @@
 {{- $id := printf "spoiler-%d" .Ordinal -}}
-<div class="spoiler {{ .Get "class" }}" {{ with .Get "style" }}style="{{ . | safeCSS }}"{{ end }}>
-  <p>
-    <a class="btn btn-primary" data-toggle="collapse" href="#{{$id}}" role="button" aria-expanded="false" aria-controls="{{$id}}">
-      {{ .Get "text" | emojify }}
-    </a>
-  </p>
-  <div class="collapse card {{ if (eq (.Get "open") "true") }}show{{ end }}" id="{{$id}}">
-    <div class="card-body">
-      {{ .Inner | markdownify | emojify }}
-    </div>
-  </div>
-</div>
+
+<details class="spoiler {{ .Get "class" }}" {{ with .Get "style" }}style="{{ . | safeCSS }}"{{ end }} id="{{$id}}">
+  <summary>{{ .Get "text" | markdownify | emojify }}</summary>
+  <p>{{ .Inner | markdownify | emojify }}</p>
+</details>

+ 3 - 0
wowchemy/test/config.yaml

@@ -1,4 +1,5 @@
 title: Wowchemy Test
+baseurl:
 module:
   imports:
     - path: github.com/wowchemy/wowchemy-hugo-modules/wowchemy
@@ -6,3 +7,5 @@ outputs:
   home: [HTML, RSS, JSON, WebAppManifest]
 params:
   require_isotope: false
+  main_menu:
+    enable: false

+ 1 - 0
wowchemy/test/content/home/index.md

@@ -1,3 +1,4 @@
 ---
 type: widget_page
+headless: true
 ---

+ 1 - 1
wowchemy/test/view.sh

@@ -1 +1 @@
-hugo server -p 1330 --minify
+hugo server -p 1330 --minify --templateMetrics --templateMetricsHints