Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions assets/css/critical.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Critical CSS — inlined into <head> via partials/head/critical-css.html.
* Keep this file small: it is rendered on every page before main.css loads.
*
* Currently scoped to text-selection highlighting so the chosen color shows
* even before the main bundle is parsed.
*/

*::selection {
background-color: var(--color-primary, #4a90e2) !important;
color: var(--color-white, #ffffff) !important;
text-shadow: none !important;
}

*::-moz-selection {
background-color: var(--color-primary, #4a90e2) !important;
color: var(--color-white, #ffffff) !important;
text-shadow: none !important;
}

html[data-theme="dark"] *::selection {
background-color: #e9996b !important;
color: #1a2633 !important;
}

html[data-theme="dark"] *::-moz-selection {
background-color: #e9996b !important;
color: #1a2633 !important;
}
119 changes: 37 additions & 82 deletions layouts/partials/head.html
Original file line number Diff line number Diff line change
@@ -1,89 +1,44 @@
<head>
{{/* SEO Critical Resources - Must come first */}}
{{ partial "seo/preload-resources.html" . }}

{{/* Consolidated Meta Tags with SEO */}}
{{ partial "head/meta.html" . }}

{{/* Structured Data */}}
{{ partial "seo/schema-org.html" . }}

<!-- Favicon and Apple Touch Icon -->
{{ partial "head/favicons.html" . }}

<!-- Critical CSS -->
{{ partial "head/critical-css.html" . }}

<!-- Main Stylesheet -->
{{ partial "head/styles.html" . }}

{{/* Math Support - Load early for better rendering */}}
{{- if .Params.math -}}
<!-- KaTeX CSS - Preload for better performance -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/katex.min.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/katex.min.css"></noscript>

<!-- KaTeX JS - Load with high priority -->
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/katex.min.js" as="script">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/contrib/auto-render.min.js" as="script">
<link rel="preload" href="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/contrib/mhchem.min.js" as="script">
{{- end -}}
{{/*
head.html — emits <head> CHILDREN ONLY.

<!-- Site Verification Meta Tags -->
{{ with site.Params.seo.google_site_verification }}
<meta name="google-site-verification" content="{{ . }}">
{{ end }}
{{ with site.Params.seo.bing_verification }}
<meta name="msvalidate.01" content="{{ . }}">
{{ end }}
{{ with site.Params.seo.yandex_verification }}
<meta name="yandex-verification" content="{{ . }}">
{{ end }}
Ownership: layouts/_default/baseof.html opens <head> and closes </head>.
Do NOT add a literal <head>/</head> here, otherwise the document would
contain nested heads.
*/}}

<!-- Include analytics scripts -->
{{ partial "analytics.html" . }}
{{/* SEO Critical Resources - Must come first */}}
{{ partial "seo/preload-resources.html" . }}

<!-- JavaScript loading - Deferred for performance -->
{{ with resources.Get "js/main.js" }}
{{ $mainJS := . | js.Build | minify | fingerprint }}
<script src="{{ $mainJS.RelPermalink }}" integrity="{{ $mainJS.Data.Integrity }}" crossorigin="anonymous" defer></script>
{{ end }}

<!-- PDF Generator -->
{{ $pdfJS := resources.Get "js/pdf-generator.js" | minify | fingerprint }}
<script src="{{ $pdfJS.RelPermalink }}" integrity="{{ $pdfJS.Data.Integrity }}" crossorigin="anonymous" defer></script>

<!-- Load scrollbar fade functionality -->
{{ $scrollbarFadeJS := resources.Get "js/scrollbar-fade.js" | minify }}
<script src="{{ $scrollbarFadeJS.RelPermalink }}" defer></script>
{{/* Consolidated Meta Tags with SEO (cached per URL — meta varies per page) */}}
{{ partialCached "head/meta.html" . .RelPermalink }}

{{/* Math rendering scripts - Load after DOM is ready */}}
{{- if .Params.math -}}
<!-- KaTeX JS -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/katex.min.js" integrity="sha384-TNnVz4NqBjFIUtubnHyyCWyGiIboUd7yCCtn7k4DwDnkWWqyOEtNKYEELv0jW+NU" crossorigin="anonymous"></script>
{{/* Structured Data */}}
{{ partial "seo/schema-org.html" . }}

<!-- KaTeX mhchem extension for chemical equations -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/contrib/mhchem.min.js" integrity="sha384-fB8BH//9nBzROkMUsu/Dr35jWHIbnKesUo9rW0hfEgw8mZGnkAyBAjKX9F98OVuo" crossorigin="anonymous"></script>
<!-- Favicon and Apple Touch Icon -->
{{ partial "head/favicons.html" . }}

<!-- KaTeX auto-render extension -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16.32/dist/contrib/auto-render.min.js" integrity="sha384-JKXHIJf8PKPyDFptuKZoUyMRQJAmQKj4B4xyOca62ebJhciMYGiDdq/9twUUWyZH" crossorigin="anonymous"></script>
<!-- Critical CSS + theme-init JS (single source of truth) -->
{{ partial "head/critical-css.html" . }}

<!-- KaTeX Configuration -->
<script>
document.addEventListener("DOMContentLoaded", function() {
renderMathInElement(document.body, {
delimiters: [
{left: '$$', right: '$$', display: true},
{left: '$', right: '$', display: false},
{left: '\\(', right: '\\)', display: false},
{left: '\\[', right: '\\]', display: true}
],
throwOnError: false,
errorColor: '#cc0000',
strict: false,
trust: true,
});
});
</script>
{{- end -}}
</head>
<!-- Main Stylesheet -->
{{ partial "head/styles.html" . }}

{{/* TODO: replaced by Unit 11 katex partial — assets/js/katex-init.js + head/katex.html */}}

<!-- Site Verification Meta Tags -->
{{ with site.Params.seo.google_site_verification }}
<meta name="google-site-verification" content="{{ . }}">
{{ end }}
{{ with site.Params.seo.bing_verification }}
<meta name="msvalidate.01" content="{{ . }}">
{{ end }}
{{ with site.Params.seo.yandex_verification }}
<meta name="yandex-verification" content="{{ . }}">
{{ end }}

<!-- Include analytics scripts -->
{{ partial "analytics.html" . }}

<!-- Application JavaScript (deferred) -->
{{ partial "head/js.html" . }}
47 changes: 11 additions & 36 deletions layouts/partials/head/critical-css.html
Original file line number Diff line number Diff line change
@@ -1,40 +1,15 @@
<script>
(function() {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const currentTheme = localStorage.getItem('theme') ||
(prefersDarkScheme.matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', currentTheme);
document.documentElement.style.backgroundColor =
currentTheme === 'dark' ? '#1a202c' : '#275f85';
document.documentElement.style.color =
currentTheme === 'dark' ? '#e2e8ff' : '#cad6ff';
})();
</script>
{{/*
critical-css.html — inlines the canonical theme-init JS (assets/js/critical.js)
and the small critical CSS bundle (assets/css/critical.css) into <head>.

This is the SINGLE place that emits the theme-init script. Other partials
must not duplicate the localStorage/data-theme bootstrap.
*/}}

{{ $criticalJS := resources.Get "js/critical.js" | js.Build | minify }}
<script>{{ $criticalJS.Content | safeJS }}</script>

<style>
/* Text selection highlighting with maximum specificity */
*::selection {
background-color: var(--color-primary, #4a90e2) !important;
color: var(--color-white, #ffffff) !important;
text-shadow: none !important;
}

*::-moz-selection {
background-color: var(--color-primary, #4a90e2) !important;
color: var(--color-white, #ffffff) !important;
text-shadow: none !important;
}

html[data-theme="dark"] *::selection {
background-color: #e9996b !important;
color: #1a2633 !important;
}

html[data-theme="dark"] *::-moz-selection {
background-color: #e9996b !important;
color: #1a2633 !important;
}
</style>
{{ with resources.Get "css/critical.css" }}
{{ $criticalCSS := . | minify }}
<style>{{ $criticalCSS.Content | safeCSS }}</style>
{{ end }}
44 changes: 21 additions & 23 deletions layouts/partials/head/js.html
Original file line number Diff line number Diff line change
@@ -1,26 +1,24 @@
<!-- Theme initialization script - must run BEFORE CSS loads -->
<script>
(function() {
const prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
const currentTheme = localStorage.getItem('theme') ||
(prefersDarkScheme.matches ? 'dark' : 'light');
document.documentElement.setAttribute('data-theme', currentTheme);

// Prevent flash of incorrect theme
document.documentElement.style.backgroundColor =
currentTheme === 'dark' ? '#1a202c' : '#275f85';
})();
</script>
{{/*
js.html — application JS bundles. Theme-init is handled in critical-css.html
via assets/js/critical.js, do not duplicate it here.

{{- with resources.Get "js/main.js" }}
{{- if eq hugo.Environment "development" }}
{{- with . | js.Build }}
<script src="{{ .RelPermalink }}"></script>
{{- end }}
{{- else }}
{{- $opts := dict "minify" true }}
{{- with . | js.Build $opts | fingerprint }}
<script src="{{ .RelPermalink }}" integrity="{{- .Data.Integrity }}" crossorigin="anonymous"></script>
{{- end }}
Production: minify, drop console/debugger, fingerprint with SRI.
Development: external sourceMap, no minify, no SRI (faster rebuilds).
*/}}

{{- $jsOpts := dict "sourceMap" "external" -}}
{{- if hugo.IsProduction -}}
{{- $jsOpts = dict "minify" true "drop" (slice "console" "debugger") -}}
{{- end -}}

{{- range (slice "js/main.js" "js/pdf-generator.js" "js/scrollbar-fade.js") }}
{{- with resources.Get . }}
{{- $built := . | js.Build $jsOpts -}}
{{- if hugo.IsProduction -}}
{{- $built = $built | fingerprint -}}
<script src="{{ $built.RelPermalink }}" integrity="{{ $built.Data.Integrity }}" crossorigin="anonymous" defer></script>
{{- else -}}
<script src="{{ $built.RelPermalink }}" defer></script>
{{- end -}}
{{- end }}
{{- end }}
20 changes: 16 additions & 4 deletions layouts/partials/head/meta.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,20 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="format-detection" content="telephone=no">

<!-- Content Security Policy -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' blob: https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://huggingface.co https://*.huggingface.co https://*.hf.co; media-src 'self' blob:; worker-src 'self' blob:;">
{{/*
Content Security Policy.
- Configurable via site.Params.security.csp (whole policy string).
- Default keeps 'unsafe-inline' on script-src because critical-css.html
inlines theme-init JS via safeJS, and on style-src for the inlined
critical CSS <style> block. Removing 'unsafe-inline' would require
moving to nonces/hashes (out of scope for Hugo 0.136).
- 'unsafe-eval' has been dropped from the default; opt back in via
site.Params.security.csp if a third-party script needs it.
*/}}
{{- $defaultCSP := "default-src 'self'; script-src 'self' 'unsafe-inline' https://cdnjs.cloudflare.com https://cdn.jsdelivr.net; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self' blob: https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://huggingface.co https://*.huggingface.co https://*.hf.co; media-src 'self' blob:; worker-src 'self' blob:;" -}}
{{- $csp := $defaultCSP -}}
{{- with site.Params.security }}{{ with .csp }}{{ $csp = . }}{{ end }}{{ end -}}
<meta http-equiv="Content-Security-Policy" content="{{ $csp }}">

{{/* Title with fallback logic */}}
{{ if .IsHome }}
Expand Down Expand Up @@ -96,10 +108,10 @@
{{/* Article specific meta tags */}}
{{ if .IsPage }}
{{ if .Date }}
<meta property="article:published_time" content="{{ .Date.Format "2006-01-02T15:04:05Z07:00" }}">
<meta property="article:published_time" content="{{ time.Format "2006-01-02T15:04:05Z07:00" .Date }}">
{{ end }}
{{ if .Lastmod }}
<meta property="article:modified_time" content="{{ .Lastmod.Format "2006-01-02T15:04:05Z07:00" }}">
<meta property="article:modified_time" content="{{ time.Format "2006-01-02T15:04:05Z07:00" .Lastmod }}">
{{ end }}
{{ with .Params.tags }}
{{ range . }}
Expand Down
Loading