diff --git a/.github/workflows/jekyll.yml b/.github/workflows/jekyll.yml
index 6d666593..5c848084 100644
--- a/.github/workflows/jekyll.yml
+++ b/.github/workflows/jekyll.yml
@@ -8,8 +8,6 @@ on:
permissions:
contents: read
- pages: write
- id-token: write
concurrency:
group: jekyll-site-${{ github.ref }}
@@ -24,7 +22,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: "3.2.2"
+ ruby-version: "3.4.10"
bundler: "2.5.23"
bundler-cache: true
@@ -38,8 +36,10 @@ jobs:
- name: Package Pages artifact
run: |
- tar --dereference --hard-dereference -cvf github-pages.tar _site
- gzip -f github-pages.tar
+ tar --dereference \
+ --directory "${GITHUB_WORKSPACE}/_site" \
+ --create --file "${GITHUB_WORKSPACE}/github-pages.tar" .
+ gzip -f "${GITHUB_WORKSPACE}/github-pages.tar"
- name: Upload artifact
uses: actions/upload-artifact@v6
@@ -54,6 +54,9 @@ jobs:
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
environment:
diff --git a/.github/workflows/rebuild-on-search-update.yml b/.github/workflows/rebuild-on-search-update.yml
index 8c41fd54..42c35c2c 100644
--- a/.github/workflows/rebuild-on-search-update.yml
+++ b/.github/workflows/rebuild-on-search-update.yml
@@ -8,8 +8,6 @@ on:
permissions:
contents: read
- pages: write
- id-token: write
jobs:
build:
@@ -20,7 +18,7 @@ jobs:
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
- ruby-version: "3.2.2"
+ ruby-version: "3.4.10"
bundler: "2.5.23"
bundler-cache: true
@@ -31,8 +29,10 @@ jobs:
- name: Package Pages artifact
run: |
- tar --dereference --hard-dereference -cvf github-pages.tar _site
- gzip -f github-pages.tar
+ tar --dereference \
+ --directory "${GITHUB_WORKSPACE}/_site" \
+ --create --file "${GITHUB_WORKSPACE}/github-pages.tar" .
+ gzip -f "${GITHUB_WORKSPACE}/github-pages.tar"
- name: Upload artifact
uses: actions/upload-artifact@v6
@@ -45,6 +45,9 @@ jobs:
deploy:
needs: build
runs-on: ubuntu-latest
+ permissions:
+ pages: write
+ id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
diff --git a/.github/workflows/teaching-content-checks.yml b/.github/workflows/teaching-content-checks.yml
index 7988d3e5..4499de09 100644
--- a/.github/workflows/teaching-content-checks.yml
+++ b/.github/workflows/teaching-content-checks.yml
@@ -20,6 +20,9 @@ on:
- "package.json"
- "package-lock.json"
+permissions:
+ contents: read
+
jobs:
markdown-quality:
runs-on: ubuntu-latest
@@ -35,16 +38,16 @@ jobs:
cache: "npm"
- name: Install dependencies
- run: npm ci
+ run: npm ci --ignore-scripts
- name: Run markdownlint for teaching content
run: |
- npx markdownlint-cli2 --config .markdownlint-cli2.jsonc \
+ ./node_modules/.bin/markdownlint-cli2 --config .markdownlint-cli2.jsonc \
"_teaching/**/*.md" \
"assets/images/teaching/README.md"
- name: Check prettier formatting for teaching content
run: |
- npx prettier --check \
+ ./node_modules/.bin/prettier --check \
"_teaching/**/*.md" \
"assets/images/teaching/README.md"
diff --git a/.jekyllignore b/.jekyllignore
index 3d477435..46476492 100644
--- a/.jekyllignore
+++ b/.jekyllignore
@@ -1,4 +1,5 @@
.cursor/
.claude/
CLAUDE.md
+AGENTS.md
.gitignore
diff --git a/.ruby-version b/.ruby-version
index be94e6f5..84d6c676 100644
--- a/.ruby-version
+++ b/.ruby-version
@@ -1 +1 @@
-3.2.2
+3.4.10
diff --git a/CNAME b/CNAME
index e143a36f..82f1fd05 100644
--- a/CNAME
+++ b/CNAME
@@ -1 +1 @@
-https://comphy-lab.org
\ No newline at end of file
+comphy-lab.org
diff --git a/Gemfile b/Gemfile
index 6fcc32f3..25a37536 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,8 +1,10 @@
source "https://rubygems.org"
-ruby "~> 3.2.0"
+ruby "~> 3.4.0"
gem "jekyll", "~> 4.3.2"
+gem "base64"
+gem "csv"
gem "webrick", "~> 1.8"
gem "jekyll-sitemap"
gem "jekyll-include-cache"
diff --git a/Gemfile.lock b/Gemfile.lock
index 6f8eb9fa..c74066ad 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -3,9 +3,11 @@ GEM
specs:
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
+ base64 (0.3.0)
bigdecimal (3.1.9)
colorator (1.1.0)
concurrent-ruby (1.3.7)
+ csv (3.3.6)
em-websocket (0.5.3)
eventmachine (>= 0.12.9)
http_parser.rb (~> 0)
@@ -176,6 +178,8 @@ PLATFORMS
x86_64-linux-musl
DEPENDENCIES
+ base64
+ csv
jekyll (~> 4.3.2)
jekyll-include-cache
jekyll-seo-tag
@@ -184,7 +188,7 @@ DEPENDENCIES
webrick (~> 1.8)
RUBY VERSION
- ruby 3.2.2p53
+ ruby 3.4.10
BUNDLED WITH
2.5.23
diff --git a/README.md b/README.md
index 656644f1..1e29b3ba 100644
--- a/README.md
+++ b/README.md
@@ -102,7 +102,7 @@ A static website for the Computational Multiphase Physics Laboratory, built with
2. **Manual Setup (Alternative)**
Prerequisites:
- - Ruby `3.2.2` (matches `.ruby-version`)
+ - Ruby `3.4.10` (matches `.ruby-version`)
- Bundler `2.5.23` (`gem install bundler -v 2.5.23`)
- Node.js and npm (for linting and testing)
@@ -839,7 +839,7 @@ The repository uses automated tools to ensure code quality and consistency:
```bash
npm install
- npx husky install
+ ./node_modules/.bin/husky install
```
#### Pre-commit Hooks
diff --git a/_config.yml b/_config.yml
index 24a1cea9..39d4e34c 100644
--- a/_config.yml
+++ b/_config.yml
@@ -45,6 +45,7 @@ exclude:
- .claude
- .claude/**
- CLAUDE.md
+ - AGENTS.md
# Design-system handoff bundle — reference only, not built
- comphy-lab-design-system
- comphy-lab-design-system/**
diff --git a/_layouts/default.html b/_layouts/default.html
index 1df94023..4def6941 100644
--- a/_layouts/default.html
+++ b/_layouts/default.html
@@ -130,7 +130,9 @@
-
+
diff --git a/_layouts/history.html b/_layouts/history.html
index ef65c281..d013e375 100644
--- a/_layouts/history.html
+++ b/_layouts/history.html
@@ -82,7 +82,9 @@
-
+
diff --git a/_layouts/join-us.html b/_layouts/join-us.html
index 38546625..929bcdf5 100644
--- a/_layouts/join-us.html
+++ b/_layouts/join-us.html
@@ -59,7 +59,9 @@
-
+
diff --git a/_layouts/team.html b/_layouts/team.html
index a1264424..ea54bd9d 100644
--- a/_layouts/team.html
+++ b/_layouts/team.html
@@ -76,7 +76,9 @@
-
+
diff --git a/about-legacy.html b/about-legacy.html
new file mode 100644
index 00000000..a41135b5
--- /dev/null
+++ b/about-legacy.html
@@ -0,0 +1,15 @@
+---
+layout: default
+title: Redirecting to CoMPhy Lab
+permalink: /about.html
+sitemap: false
+---
+
+
+
+
+
About has moved to the homepage.
+
If you are not redirected automatically, click here.
+
diff --git a/assets/css/join-us.css b/assets/css/join-us.css
index 73e2f660..134d2d54 100644
--- a/assets/css/join-us.css
+++ b/assets/css/join-us.css
@@ -202,7 +202,7 @@
margin: 4rem 0;
}
-.project-category h3 {
+.project-category > h3 {
color: var(--join-subheading-color);
font-size: 2.2rem;
margin-bottom: 2rem;
@@ -211,16 +211,16 @@
padding-bottom: 0;
}
-.project-category h3:after {
+.project-category > h3:after {
content: none;
}
/* Different accent colors for each category */
-.soft-matter-category h3:after {
+.soft-matter-category > h3:after {
background-color: var(--join-phd-accent);
}
-.free-surface-category h3:after {
+.free-surface-category > h3:after {
background-color: var(--join-postdoc-accent);
}
@@ -321,7 +321,7 @@
font-size: 2.4rem;
}
- .project-category h3 {
+ .project-category > h3 {
font-size: 2rem;
}
}
diff --git a/assets/css/research.css b/assets/css/research.css
index c779c347..2ba98fec 100644
--- a/assets/css/research.css
+++ b/assets/css/research.css
@@ -700,7 +700,7 @@
}
.research-content .tags span:nth-of-type(3) {
- background: var(--color-primary-alpha-5);
+ background: var(--color-primary-alpha-05);
}
[data-theme="dark"] .research-content .tags span:nth-of-type(3) {
@@ -708,7 +708,7 @@
}
.research-content .tags span:nth-of-type(n + 4) {
- background: var(--color-primary-alpha-5);
+ background: var(--color-primary-alpha-05);
}
[data-theme="dark"] .research-content .tags span:nth-of-type(n + 4) {
diff --git a/assets/css/styles.css b/assets/css/styles.css
index 899ddba3..f5e4f93c 100644
--- a/assets/css/styles.css
+++ b/assets/css/styles.css
@@ -76,6 +76,7 @@
/* Color System - Transparent Colors */
--color-black-alpha-05: rgba(0, 0, 0, 0.05);
+ --color-black-alpha-08: rgba(0, 0, 0, 0.08);
--color-black-alpha-10: rgba(0, 0, 0, 0.1);
--color-black-alpha-15: rgba(0, 0, 0, 0.15);
--color-black-alpha-20: rgba(0, 0, 0, 0.2);
@@ -89,6 +90,7 @@
--color-white-alpha-30: rgba(255, 255, 255, 0.3);
--color-white-alpha-35: rgba(255, 255, 255, 0.35);
--color-white-alpha-40: rgba(255, 255, 255, 0.4);
+ --color-white-alpha-50: rgba(255, 255, 255, 0.5);
--color-white-alpha-70: rgba(255, 255, 255, 0.7);
--color-white-alpha-85: rgba(255, 255, 255, 0.85);
--color-white-alpha-95: rgba(255, 255, 255, 0.95);
@@ -105,7 +107,9 @@
--color-blue-bright-alpha-20: rgba(76, 110, 245, 0.2);
--color-blue-bright-alpha-25: rgba(76, 110, 245, 0.25);
--color-blue-bright-alpha-30: rgba(76, 110, 245, 0.3);
+ --color-blue-bright-alpha-40: rgba(76, 110, 245, 0.4);
--color-blue-bright-alpha-50: rgba(76, 110, 245, 0.5);
+ --color-blue-bright-alpha-80: rgba(76, 110, 245, 0.8);
--color-red-darker-alpha-20: rgba(250, 82, 82, 0.2);
--color-header-base-alpha-15: rgba(104, 35, 109, 0.15);
--color-header-base-alpha-20: rgba(104, 35, 109, 0.2);
@@ -129,6 +133,7 @@
/* Transitions */
--transition-fast: all 0.2s ease;
--transition-medium: all 0.3s ease;
+ --loader-border-color: var(--color-black-alpha-10);
/* Spacing System */
--space-xs: 0.5rem;
@@ -2323,7 +2328,7 @@ section#featured:after,
}
/* Join page markdown refinements */
-.join-content .markdown-content h2 {
+.join-content.markdown-content h2 {
font-size: 2.4rem;
color: var(--color-primary);
margin: 2.4rem 0 1.2rem;
@@ -2331,17 +2336,17 @@ section#featured:after,
border-bottom: 2px solid var(--position-h2-border);
}
-.join-content .markdown-content p {
+.join-content.markdown-content p {
margin: 0 0 1.2rem;
}
-.join-content .markdown-content ul,
-.join-content .markdown-content ol {
+.join-content.markdown-content ul,
+.join-content.markdown-content ol {
margin: 0 0 1.2rem 2rem;
padding: 0;
}
-.join-content .markdown-content li {
+.join-content.markdown-content li {
margin: 0.4rem 0;
}
diff --git a/assets/js/main.js b/assets/js/main.js
index 841549c7..ee76d91d 100644
--- a/assets/js/main.js
+++ b/assets/js/main.js
@@ -445,7 +445,7 @@
// If not on home page, let the browser handle the navigation
} else if (href.startsWith("#")) {
e.preventDefault();
- const target = document.querySelector(href);
+ const target = document.getElementById(href.substring(1));
if (target) {
target.scrollIntoView({
behavior: "smooth",
diff --git a/assets/js/search-manager.js b/assets/js/search-manager.js
index e86047dc..2617c181 100644
--- a/assets/js/search-manager.js
+++ b/assets/js/search-manager.js
@@ -32,6 +32,29 @@
/^(?:Private|0_todo|Admin|Medical|Personal)\b/i,
];
+ const PUBLIC_SEARCH_HOSTS = new Set([
+ "comphy-lab.org",
+ "blogs.comphy-lab.org",
+ ]);
+
+ function isSafeSearchUrl(value) {
+ if (typeof value !== "string" || value.length === 0) {
+ return false;
+ }
+
+ try {
+ const parsed = new URL(value, window.location.origin);
+ return (
+ (parsed.protocol === "https:" || parsed.protocol === "http:") &&
+ PUBLIC_SEARCH_HOSTS.has(parsed.hostname) &&
+ parsed.username === "" &&
+ parsed.password === ""
+ );
+ } catch (_error) {
+ return false;
+ }
+ }
+
function isInternalSearchEntry(entry) {
const entryIdentityText = [entry?.title, entry?.url]
.filter(Boolean)
@@ -43,7 +66,10 @@
}
function sanitizeSearchData(data) {
- return data.filter((entry) => !isInternalSearchEntry(entry));
+ return data.filter(
+ (entry) =>
+ !isInternalSearchEntry(entry) && isSafeSearchUrl(entry?.url)
+ );
}
/**
@@ -199,7 +225,7 @@
id: `search-result-${result.refIndex}`,
title: result.item.title || "Untitled",
handler: () => {
- if (result.item.url) {
+ if (isSafeSearchUrl(result.item.url)) {
window.location.href = result.item.url;
}
},
diff --git a/eslint.config.js b/eslint.config.js
index 88b7d4f4..b2d277a4 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,13 +1,18 @@
+const js = require("@eslint/js");
+const globals = require("globals");
+
module.exports = [
+ js.configs.recommended,
{
languageOptions: {
- ecmaVersion: 12,
- sourceType: "module",
+ ecmaVersion: 2021,
+ sourceType: "script",
globals: {
- browser: true,
- es2021: true,
+ ...globals.browser,
+ ...globals.es2021,
marked: "readonly",
- DOMPurify: "readonly"
+ DOMPurify: "readonly",
+ Utils: "readonly"
}
},
rules: {
diff --git a/package-lock.json b/package-lock.json
index acf0ef78..a54b8011 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,7 +9,9 @@
"version": "1.0.0",
"license": "MIT",
"devDependencies": {
+ "@eslint/js": "8.57.1",
"eslint": "^8.56.0",
+ "globals": "13.24.0",
"husky": "^8.0.3",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
diff --git a/package.json b/package.json
index 58716b66..0bd63bce 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"lint:md": "markdownlint-cli2 --config .markdownlint-cli2.jsonc",
"lint": "npm run lint:js && npm run lint:css && npm run lint:md",
"format": "prettier --write assets/js/**/*.js assets/css/**/*.css",
- "prepare": "npx husky install",
+ "prepare": "husky install",
"test": "jest --coverage"
},
"repository": {
@@ -29,7 +29,9 @@
},
"homepage": "https://comphy-lab.org",
"devDependencies": {
+ "@eslint/js": "8.57.1",
"eslint": "^8.56.0",
+ "globals": "13.24.0",
"husky": "^8.0.3",
"jest": "^30.3.0",
"jest-environment-jsdom": "^30.3.0",
diff --git a/scripts/fix-line-length.js b/scripts/fix-line-length.js
index 9ca0c86b..73ee6c34 100644
--- a/scripts/fix-line-length.js
+++ b/scripts/fix-line-length.js
@@ -217,6 +217,7 @@ function processJsFiles() {
fixedFiles++;
} catch (error) {
console.error(`Error writing to file ${file}:`, error.message);
+ throw error;
}
}
}
@@ -228,4 +229,4 @@ function processJsFiles() {
}
// Run the script
-processJsFiles();
\ No newline at end of file
+processJsFiles();
diff --git a/scripts/generate_seo_tags.rb b/scripts/generate_seo_tags.rb
index 1f0be6ec..59dee1d2 100755
--- a/scripts/generate_seo_tags.rb
+++ b/scripts/generate_seo_tags.rb
@@ -87,10 +87,9 @@ def normalize_url(url)
end
normalized = Pathname.new(url).cleanpath.to_s
- return nil unless normalized.start_with?('/')
-
# Remove leading slash for file operations
result = normalized.sub(/^\//, '')
+ return nil if result.start_with?('~')
# Debug output
puts "URL Normalization: #{original_url} -> #{result}" if ENV['DEBUG']
@@ -285,8 +284,8 @@ def update_description_meta(doc, head, description)
descriptions = descriptions_by_url[url] || Set.new
# Get file path
- file_path = File.expand_path(url, site_dir)
site_root = File.expand_path(site_dir)
+ file_path = File.expand_path(File.join(site_root, url))
unless file_path.start_with?("#{site_root}#{File::SEPARATOR}")
puts "Skipping unsafe output path for #{url}" if ENV['DEBUG']
diff --git a/scripts/lint-check.sh b/scripts/lint-check.sh
index b0e428e2..54eca9fc 100755
--- a/scripts/lint-check.sh
+++ b/scripts/lint-check.sh
@@ -110,9 +110,15 @@ fi
# Run ESLint on JavaScript files with quotes rule
JS_DIR="$REPO_ROOT/assets/js"
+ESLINT_BIN="$REPO_ROOT/node_modules/.bin/eslint"
+
+if [[ ! -x "$ESLINT_BIN" ]]; then
+ echo "Local ESLint is unavailable. Run npm ci first." >&2
+ exit 1
+fi
if [[ "$FIX_MODE" == "true" ]]; then
# Fix mode: run ESLint with --fix flag
- npx eslint "$JS_DIR" --rule 'quotes: ["error", "double"]' --fix
+ "$ESLINT_BIN" "$JS_DIR" --rule 'quotes: ["error", "double"]' --fix
if [ $? -eq 0 ]; then
echo "Fixed quote style in JavaScript files."
else
@@ -120,7 +126,7 @@ if [[ "$FIX_MODE" == "true" ]]; then
fi
else
# Check-only mode: run ESLint without --fix
- npx eslint "$JS_DIR" --rule 'quotes: ["error", "double"]'
+ "$ESLINT_BIN" "$JS_DIR" --rule 'quotes: ["error", "double"]'
ESLINT_EXIT_CODE=$?
if [ $ESLINT_EXIT_CODE -eq 0 ]; then
echo "No quote style issues found in JavaScript files."
@@ -130,4 +136,4 @@ else
fi
fi
-echo "Lint check completed!"
\ No newline at end of file
+echo "Lint check completed!"
diff --git a/scripts/setup.sh b/scripts/setup.sh
index 95c5603b..fc26b9f8 100755
--- a/scripts/setup.sh
+++ b/scripts/setup.sh
@@ -151,7 +151,7 @@ echo "🔨 Building site and generating search database..."
# Install husky hooks
echo ""
echo "🪝 Installing Git hooks (husky)..."
-npx husky install
+./node_modules/.bin/husky install
# Run simple validation tests
echo ""
diff --git a/tests/fix-line-length-actual.test.js b/tests/fix-line-length-actual.test.js
index 50a2ef91..e7a75438 100644
--- a/tests/fix-line-length-actual.test.js
+++ b/tests/fix-line-length-actual.test.js
@@ -118,6 +118,20 @@ describe('fix-line-length.js actual implementation', () => {
expect(mockFs.writeFileSync).toHaveBeenCalledTimes(0);
});
+ it('should fail when a rewritten file cannot be written', () => {
+ mockFs.readdirSync.mockReturnValue(['long-lines.js']);
+ mockFs.readFileSync.mockReturnValue(
+ 'const value = {first: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", second: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"};'
+ );
+ mockFs.writeFileSync.mockImplementation(() => {
+ throw new Error('Permission denied');
+ });
+
+ expect(() => {
+ require('../scripts/fix-line-length.js');
+ }).toThrow('Permission denied');
+ });
+
it('should log summary of processed files', () => {
mockFs.readdirSync.mockReturnValue(['test1.js', 'test2.js']);
mockFs.readFileSync.mockReturnValue('short line');
@@ -129,4 +143,4 @@ describe('fix-line-length.js actual implementation', () => {
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('0 lines exceeded 80 characters'));
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('0 files were modified'));
});
-});
\ No newline at end of file
+});
diff --git a/tests/search-manager.test.js b/tests/search-manager.test.js
index 229245b1..1c303bb2 100644
--- a/tests/search-manager.test.js
+++ b/tests/search-manager.test.js
@@ -79,4 +79,40 @@ describe("search-manager sanitization", () => {
"https://comphy-lab.org/.agents/skills/add-paper/SKILL/#badge-shapes"
);
});
+
+ it("rejects unsafe and unapproved search result URLs", async () => {
+ global.fetch = jest.fn().mockResolvedValue({
+ ok: true,
+ json: () =>
+ Promise.resolve([
+ {
+ title: "Script URL",
+ url: "javascript:alert(document.domain)",
+ content: "malicious search entry",
+ },
+ {
+ title: "Credentialed URL",
+ url: "https://attacker@comphy-lab.org/",
+ content: "misleading search entry",
+ },
+ {
+ title: "External redirect",
+ url: "https://example.net/phishing",
+ content: "unapproved search entry",
+ },
+ {
+ title: "Public blog post",
+ url: "https://blogs.comphy-lab.org/public-post/",
+ content: "approved search entry",
+ },
+ ]),
+ });
+
+ require("../assets/js/search-manager.js");
+
+ const data = await window.SearchManager.loadSearchDatabase();
+
+ expect(data).toHaveLength(1);
+ expect(data[0].title).toBe("Public blog post");
+ });
});