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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .codex/skills/github-pages-academic-portfolio/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
---
name: github-pages-academic-portfolio
description: Design, audit, optimize, and maintain evidence-based academic or technical portfolios hosted on GitHub Pages, especially Jekyll and al-folio sites. Use for portfolio information architecture, visual systems, responsive components, accessibility, SEO, performance, GitHub Actions, or pull-request delivery; do not use for unrelated application development.
---

# GitHub Pages Academic Portfolio

Build a portfolio that makes the person's research direction, technical capability, and evidence easy to understand. Treat visual polish, accessibility, content integrity, and maintainability as one system.

## Start from repository evidence

1. Inspect the repository root, current branch, status, remote, theme/runtime version, page collection, layouts, styles, build workflow, and existing content conventions.
2. Preserve unrelated work and existing URLs. Prefer a focused feature branch.
3. Identify which files are owned locally and which are supplied by the theme or a gem. Prefer additive site-level layouts and partials to large upstream overrides. If an override is necessary, document why and keep it narrow.
4. Treat profile and CV claims as user-provided evidence. Do not invent publications, affiliations, metrics, credentials, project outcomes, or proficiency levels.
5. Never publish private source documents, salary data, personal addresses, signatures, identification numbers, or contract administration details.

## Choose the smallest useful mode

- **Audit:** assess information hierarchy, visual consistency, responsiveness, accessibility, SEO, performance, content evidence, and deployment reliability.
- **Design system:** define tokens, typography, spacing, surfaces, focus states, cards, navigation, buttons, and light/dark behavior before styling individual pages.
- **Portfolio implementation:** improve the homepage and relevant pages while preserving validated content and stable permalinks.
- **Content refinement:** structure each project around problem, method, validation, result, limitations, and resources. Separate verified outcomes from planned work.
- **Delivery:** validate locally, prepare a focused commit, and create or update a pull request only when remote writes are authorized.

Read [design system](references/design-system.md) before selecting colors or changing site-wide styling. Read [content and SEO](references/content-and-seo.md) when changing page structure, professional positioning, metadata, or project narratives. Read [validation](references/validation.md) before declaring the work complete.

## Implementation rules

- Use semantic HTML and native navigation landmarks. Keep one clear page heading and a logical heading hierarchy.
- Centralize shared visual rules. Do not duplicate a full design system inside individual page files.
- Use CSS custom properties as the public token layer. Define both light and dark values and keep component rules token-driven.
- Keep body copy comfortable at 16px-equivalent or larger in the browser. Limit prose width to roughly 65–75 characters.
- Use responsive grids that collapse without horizontal scrolling. Verify at mobile, tablet, desktop, and 200% text zoom.
- Make link, button, hover, active, and keyboard-focus states visibly distinct. Do not communicate meaning through color alone.
- Prefer restrained motion. Respect `prefers-reduced-motion` and avoid decorative animation that delays content.
- Optimize images with correct dimensions, descriptive alt text, and lazy loading except for the primary above-the-fold profile image.
- Keep external links explicit and safe. Use `rel="noopener noreferrer"` for new-tab links.
- Preserve working dark mode, search, CV download, structured metadata, canonical URLs, and deployment paths.

## Quality gates

Before handoff:

1. Run the repository formatter and production build.
2. Run `scripts/audit_portfolio.py REPOSITORY_ROOT` from this skill.
3. Check every foreground/background pair with `scripts/check_contrast.py`; normal text must reach 4.5:1 and large text or essential UI graphics 3:1.
4. Inspect the rendered homepage and every materially changed page at desktop and mobile widths, in light and dark modes.
5. Verify keyboard navigation, visible focus, image alt text, headings, landmarks, internal links, CV download, and external profile links.
6. Review the full branch diff for private information, unrelated files, generated clutter, and unsupported claims.
7. Report checks actually run, remaining limitations, files changed, and remote actions performed.

Do not describe a CI configuration as passing until the corresponding run completes successfully. If a production build cannot run locally, state that clearly and require the pull-request build to pass before recommending merge.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
interface:
display_name: "Academic Portfolio"
short_description: "Design and optimize an academic GitHub portfolio"
brand_color: "#0B5CAD"
default_prompt: "Use $github-pages-academic-portfolio to improve my GitHub Pages academic portfolio."
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Portfolio content and SEO

## Information architecture

The homepage should answer, in order:

1. Who is this person professionally?
2. What problems and research areas do they work on now?
3. What evidence demonstrates their capability?
4. Where can a reviewer inspect projects, research, CV, code, or contact details?

Keep the primary navigation small. Prefer stable sections such as About, Research, Projects, CV, and Contact. Avoid duplicate labels and near-duplicate pages.

## Academic and technical credibility

- State the current academic or research role precisely.
- Distinguish active research, completed work, and future interests.
- Present open-source contributions as contributions, not as ownership of upstream software.
- Do not call work peer reviewed, published, deployed, production-grade, or state of the art without evidence.
- Use measured results only when the evaluation design and comparison context are available.
- For machine-learning projects, disclose validation strategy and important generalization limitations.

## Project narrative

Use this order when the evidence supports it:

- **Problem:** the technical or scientific question.
- **Method:** data, models, software, and workflow.
- **Validation:** splits, baselines, cross-validation, reproducibility, or parity checks.
- **Result:** concise quantitative or qualitative outcome.
- **Limitation:** the main boundary on interpretation.
- **Resources:** repository, report, demonstration, or publication.

## Search and social metadata

- Give every indexable page a unique title and accurate description.
- Keep canonical URLs and sitemap generation enabled.
- Provide Open Graph metadata and a valid share image where supported.
- Use descriptive link text instead of repeated “click here”.
- Use one H1 per page and a logical H2/H3 hierarchy.
- Keep URLs stable during redesigns; add redirects only when a URL must change.
- Include person and website structured data only when fields are accurate and publicly intended.

## Privacy

Publish professional contact channels intentionally selected by the user. Do not infer or expose private email addresses, phone numbers, home addresses, employment contract details, salary information, signatures, or identifiers from source documents.
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Academic portfolio design system

## Visual direction

Use a restrained institutional palette rather than a high-saturation product-marketing palette. A reliable pattern is:

- deep navy for authority, headings, and the primary dark surface;
- accessible blue for links and primary actions;
- teal for scientific or research emphasis;
- warm gold only for small decorative accents, never as low-contrast body text;
- cool off-white and blue-grey neutrals for reading surfaces and borders.

The palette should support the content hierarchy, not become the subject of the page. Use one dominant accent and one secondary accent per component.

## Recommended token model

Define semantic tokens rather than embedding colors in components:

- `--global-bg-color`, `--global-card-bg-color`, `--global-text-color`, `--global-text-color-light`;
- `--global-theme-color`, `--global-hover-color`, `--global-divider-color` for theme compatibility;
- site tokens such as `--academic-navy`, `--academic-teal`, `--academic-gold`, `--academic-surface-soft`, `--academic-shadow`, and `--academic-focus`;
- spacing, radius, maximum reading width, and transition tokens.

Every token that changes meaning between light and dark modes needs an explicit dark value. Component rules should consume tokens and should not repeat raw hex values except for fixed transparent overlays.

## Typography

- Prefer a neutral humanist sans serif for navigation and body text.
- A restrained serif may be used for major academic headings if it remains readable and loads efficiently.
- Keep line height around 1.55–1.75 for prose and 1.1–1.3 for headings.
- Use weight, size, spacing, and structure together; do not rely only on color.
- Avoid justified body text and overly narrow columns.

## Components

- **Navigation:** quiet surface, strong active state, adequate targets, stable mobile collapse, and clear focus rings.
- **Hero:** one career thesis, one short supporting paragraph, two primary actions at most, then compact evidence or focus labels.
- **Cards:** subtle border and shadow, consistent radii, no excessive hover lift, and no interaction styling on non-clickable cards.
- **Project evidence:** expose method, validation, result, limitation, and source links without burying them in decorative UI.
- **Calls to action:** primary action uses the accessible blue; secondary action uses a bordered surface. Both require hover and focus states.
- **Photography:** use one professional profile image with a quiet frame; do not add generic stock photography to technical portfolio pages.

## Accessibility invariants

- Normal text contrast: at least 4.5:1.
- Large text and essential non-text UI contrast: at least 3:1.
- Focus indicator: visible at 2px or stronger and distinguishable from the component and its surrounding surface.
- Target size: aim for at least 44px in the primary navigation and calls to action.
- Motion: disable non-essential transforms and transitions under `prefers-reduced-motion: reduce`.
- Zoom/reflow: content must remain usable at 200% text zoom and narrow mobile widths.

Use `scripts/check_contrast.py` for deterministic color-pair checks before relying on visual judgement.
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Validation and delivery

## Local checks

Run the repository's own commands first. For a typical Jekyll portfolio:

```bash
npm ci
npm run lint:prettier
JEKYLL_ENV=production bundle exec jekyll build
```

Then run the skill checks:

```bash
python SKILL_ROOT/scripts/audit_portfolio.py REPOSITORY_ROOT
python SKILL_ROOT/scripts/check_contrast.py --pair "body:#172033:#f7f9fc:4.5" --pair "link:#0b5cad:#ffffff:4.5"
```

Use the actual skill and repository paths; do not copy the placeholders literally.

## Rendered review

Inspect at minimum:

- homepage, Research, Projects, CV, and Contact;
- 375px, 768px, and wide desktop viewports;
- light and dark themes;
- keyboard-only navigation and visible focus;
- text zoom or browser zoom at 200%;
- profile image, internal links, project links, social profiles, and CV download;
- heading order, landmarks, accessible names, and alt text;
- overflow, clipping, awkward line breaks, layout shift, and insufficient card spacing.

When available, run Lighthouse or an equivalent accessibility/performance audit against the production build. Treat automated scores as signals, not proof of usability.

## Pull request

Keep the branch focused. The pull request should explain:

- the design and information-hierarchy problem;
- the new token system and major component changes;
- content or URL changes, if any;
- accessibility, responsive, performance, and build validation;
- theme overrides and future-upgrade implications;
- screenshots or preview artifacts when available;
- rollback, normally reverting the focused merge commit.

Wait for required checks. Fix failures on the same branch, rerun validation, and update the pull request rather than opening replacement pull requests for the same change.
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env python3
"""Run dependency-free structural checks for a Jekyll academic portfolio."""

from __future__ import annotations

import argparse
import re
import sys
from collections import Counter
from pathlib import Path


FRONT_MATTER = re.compile(r"\A---\s*\n(?P<header>.*?)\n---\s*\n", re.DOTALL)
FIELD = re.compile(r"^(?P<key>[A-Za-z_][\w-]*):\s*(?P<value>.*?)\s*$")
ROOT_LINK = re.compile(r"(?:href=[\"']|\]\()(?P<path>/[^\"')#?\s]*)(?:#[^\"')\s]*)?")


def front_matter(path: Path) -> dict[str, str]:
text = path.read_text(encoding="utf-8")
match = FRONT_MATTER.match(text)
if not match:
raise ValueError("missing YAML front matter")
fields: dict[str, str] = {}
for line in match.group("header").splitlines():
field = FIELD.match(line)
if field:
fields[field.group("key")] = field.group("value").strip('"\'')
return fields


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("repository", nargs="?", default=".", type=Path)
args = parser.parse_args()
root = args.repository.resolve()
errors: list[str] = []
warnings: list[str] = []

required = ("_config.yml", "Gemfile", "_pages", ".github/workflows/deploy.yml")
for relative in required:
if not (root / relative).exists():
errors.append(f"missing required path: {relative}")

pages: list[tuple[Path, dict[str, str]]] = []
for path in sorted((root / "_pages").glob("*.md")):
try:
fields = front_matter(path)
except ValueError as error:
errors.append(f"{path.relative_to(root)}: {error}")
continue
pages.append((path, fields))
if not fields.get("title"):
errors.append(f"{path.relative_to(root)}: missing title")
if not fields.get("permalink"):
warnings.append(f"{path.relative_to(root)}: missing explicit permalink")

permalinks = [fields.get("permalink") for _, fields in pages if fields.get("permalink")]
for permalink, count in Counter(permalinks).items():
if count > 1:
errors.append(f"duplicate permalink {permalink!r} appears {count} times")

nav_titles = [
fields.get("title", "").casefold()
for _, fields in pages
if fields.get("nav", "").casefold() == "true"
]
for title, count in Counter(nav_titles).items():
if title and count > 1:
errors.append(f"duplicate navigation title {title!r} appears {count} times")

known_routes = set(permalinks)
known_routes.add("/")
for path, _ in pages:
text = path.read_text(encoding="utf-8")
for match in ROOT_LINK.finditer(text):
route = match.group("path")
if route.startswith("//") or "." in Path(route).name:
continue
normalized = route if route.endswith("/") else f"{route}/"
if normalized not in known_routes:
warnings.append(f"{path.relative_to(root)}: unresolved root-relative route {route}")

profile = root / "assets/img/sabuj.jpg"
if not profile.exists():
warnings.append("expected profile image assets/img/sabuj.jpg was not found")

for path in root.rglob("*.md"):
if any(part in {".git", "node_modules", "tmp", "vendor"} for part in path.parts):
continue
text = path.read_text(encoding="utf-8", errors="replace").casefold()
if "lorem ipsum" in text or "[todo" in text:
errors.append(f"placeholder content remains in {path.relative_to(root)}")

for item in errors:
print(f"ERROR: {item}")
for item in sorted(set(warnings)):
print(f"WARN: {item}")
print(f"Audited {len(pages)} page files: {len(errors)} error(s), {len(set(warnings))} warning(s)")
return 1 if errors else 0


if __name__ == "__main__":
sys.exit(main())
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env python3
"""Check WCAG contrast ratios for one or more named color pairs."""

from __future__ import annotations

import argparse
import re
import sys


HEX_COLOR = re.compile(r"^#(?P<value>[0-9a-fA-F]{6})$")


def parse_color(value: str) -> tuple[int, int, int]:
match = HEX_COLOR.fullmatch(value)
if not match:
raise argparse.ArgumentTypeError(f"Expected a six-digit hex color, got {value!r}")
raw = match.group("value")
return tuple(int(raw[index : index + 2], 16) for index in (0, 2, 4))


def channel_luminance(channel: int) -> float:
value = channel / 255
return value / 12.92 if value <= 0.04045 else ((value + 0.055) / 1.055) ** 2.4


def relative_luminance(color: tuple[int, int, int]) -> float:
red, green, blue = (channel_luminance(channel) for channel in color)
return 0.2126 * red + 0.7152 * green + 0.0722 * blue


def contrast_ratio(foreground: tuple[int, int, int], background: tuple[int, int, int]) -> float:
lighter, darker = sorted(
(relative_luminance(foreground), relative_luminance(background)), reverse=True
)
return (lighter + 0.05) / (darker + 0.05)


def parse_pair(value: str) -> tuple[str, str, str, float]:
try:
name, foreground, background, minimum = value.split(":", maxsplit=3)
parse_color(foreground)
parse_color(background)
threshold = float(minimum)
except (ValueError, argparse.ArgumentTypeError) as error:
raise argparse.ArgumentTypeError(
"Pair must be NAME:#RRGGBB:#RRGGBB:MINIMUM, for example body:#172033:#f7f9fc:4.5"
) from error
if not name.strip() or threshold <= 0:
raise argparse.ArgumentTypeError("Pair name must be non-empty and minimum must be positive")
return name.strip(), foreground, background, threshold


def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--pair", action="append", required=True, type=parse_pair)
args = parser.parse_args()

failed = False
for name, foreground, background, minimum in args.pair:
ratio = contrast_ratio(parse_color(foreground), parse_color(background))
passed = ratio >= minimum
failed |= not passed
status = "PASS" if passed else "FAIL"
print(f"{status} {name}: {ratio:.2f}:1 (minimum {minimum:.1f}:1)")

return 1 if failed else 0


if __name__ == "__main__":
sys.exit(main())
Loading
Loading