greenpage 0.1.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,13 @@
1
+ ---
2
+ name: Bug report
3
+ about: Something isn't measured or reported correctly
4
+ labels: bug
5
+ ---
6
+
7
+ **Command run**
8
+
9
+ **Expected**
10
+
11
+ **Actual**
12
+
13
+ **greenpage version / Python version / OS**
@@ -0,0 +1,9 @@
1
+ ---
2
+ name: Feature request
3
+ about: Suggest an improvement
4
+ labels: enhancement
5
+ ---
6
+
7
+ **Problem this solves**
8
+
9
+ **Proposed solution**
@@ -0,0 +1,30 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ jobs:
9
+ test:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python-version: ["3.10", "3.11", "3.12", "3.13"]
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+ - uses: actions/setup-python@v5
17
+ with:
18
+ python-version: ${{ matrix.python-version }}
19
+ - run: pip install -e '.[dev]'
20
+ - run: ruff check .
21
+ - run: pytest -q
22
+
23
+ dogfood:
24
+ runs-on: ubuntu-latest
25
+ steps:
26
+ - uses: actions/checkout@v4
27
+ - uses: ./
28
+ with:
29
+ target: tests/fixtures/site
30
+ budget: "0.1"
@@ -0,0 +1,37 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ build:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ - uses: actions/setup-python@v5
14
+ with:
15
+ python-version: "3.12"
16
+ - run: pip install build twine
17
+ - run: python -m build
18
+ - run: twine check --strict dist/*
19
+ - uses: actions/upload-artifact@v4
20
+ with:
21
+ name: dist
22
+ path: dist/
23
+
24
+ publish:
25
+ needs: build
26
+ runs-on: ubuntu-latest
27
+ environment:
28
+ name: pypi
29
+ url: https://pypi.org/p/greenpage
30
+ permissions:
31
+ id-token: write
32
+ steps:
33
+ - uses: actions/download-artifact@v4
34
+ with:
35
+ name: dist
36
+ path: dist/
37
+ - uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,7 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .pytest_cache/
7
+ .ruff_cache/
@@ -0,0 +1,23 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here, following [Keep a Changelog](https://keepachangelog.com/).
4
+
5
+ ## [0.1.1] - 2026-11-15
6
+
7
+ ### Fixed
8
+ - Green hosting now follows SWD v4: data-centre operational emissions are scaled by
9
+ `1 - green hosting factor` instead of using a 50 g/kWh renewable intensity (a v3 carry-over).
10
+
11
+ ### Added
12
+ - `green_hosting_factor` parameter (0–1) in the Python API.
13
+ - Release workflow that publishes to PyPI via Trusted Publishing.
14
+
15
+ ## [0.1.0] - 2026-11-15
16
+
17
+ ### Added
18
+ - `greenpage scan` for HTML files, static build directories and live URLs.
19
+ - Sustainable Web Design Model v4 carbon estimates with A+–F ratings.
20
+ - `--budget` option with CI-friendly exit codes.
21
+ - Text, JSON and Markdown output formats.
22
+ - Optimisation suggestions for images, fonts, media and JavaScript.
23
+ - Composite GitHub Action.
@@ -0,0 +1,10 @@
1
+ # Code of Conduct
2
+
3
+ This project follows the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
4
+
5
+ In short: be respectful, be inclusive, assume good faith, and give constructive feedback.
6
+ Harassment of any kind is not tolerated.
7
+
8
+ Report unacceptable behaviour to the maintainers by opening a private security advisory
9
+ or emailing the address listed on the maintainer's GitHub profile. All reports will be
10
+ handled confidentially.
@@ -0,0 +1,28 @@
1
+ # Contributing to greenpage
2
+
3
+ Thanks for helping make the web lighter! All contributions are welcome: code, docs, bug reports and methodology reviews.
4
+
5
+ ## Development setup
6
+
7
+ ```bash
8
+ git clone https://github.com/Amxr-tech/greenpage && cd greenpage
9
+ python -m venv .venv && source .venv/bin/activate
10
+ pip install -e '.[dev]'
11
+ pytest
12
+ ruff check .
13
+ ```
14
+
15
+ ## Guidelines
16
+
17
+ - **No runtime dependencies.** greenpage uses only the standard library so it installs anywhere, including CI.
18
+ - **Tests required.** Every behaviour change needs a test. Local-mode tests use `tests/fixtures/site`; avoid network calls in tests.
19
+ - **Methodology changes** (anything in `model.py`) must cite a source in the PR description.
20
+ - Keep pull requests focused. Open an issue first for large changes.
21
+
22
+ ## Good first issues
23
+
24
+ Look for the `good first issue` label, or pick an item from the README roadmap.
25
+
26
+ ## Code of Conduct
27
+
28
+ By participating you agree to follow our [Code of Conduct](CODE_OF_CONDUCT.md).
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 greenpage contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,144 @@
1
+ Metadata-Version: 2.5
2
+ Name: greenpage
3
+ Version: 0.1.1
4
+ Summary: Estimate and budget the carbon footprint of web pages, locally or in CI.
5
+ Author: greenpage contributors
6
+ License-Expression: MIT
7
+ License-File: LICENSE
8
+ Keywords: carbon,ci,green-software,sustainability,web-performance
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Topic :: Internet :: WWW/HTTP :: Site Management
14
+ Classifier: Topic :: Software Development :: Quality Assurance
15
+ Requires-Python: >=3.10
16
+ Provides-Extra: dev
17
+ Requires-Dist: pytest>=8; extra == 'dev'
18
+ Requires-Dist: ruff>=0.6; extra == 'dev'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # 🌱 greenpage
22
+
23
+ **Estimate and budget the carbon footprint of web pages, locally or in CI.**
24
+
25
+ [![CI](https://github.com/Amxr-tech/greenpage/actions/workflows/ci.yml/badge.svg)](https://github.com/Amxr-tech/greenpage/actions/workflows/ci.yml)
26
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
27
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
28
+ ![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
29
+
30
+ The internet accounts for an estimated 3–4% of global greenhouse gas emissions,
31
+ about the same as the aviation industry. Every byte a page sends uses energy in data
32
+ centres, networks and on the visitor's device. Teams already set **performance budgets**;
33
+ greenpage adds a **carbon budget** that stops pages from quietly getting heavier.
34
+
35
+ - 📏 **Measures** every page in a static build (`dist/`, `public/`, `_site/`) offline, or any live URL
36
+ - 🧮 **Estimates** grams of CO₂e per page view using the open [Sustainable Web Design Model v4](https://sustainablewebdesign.org/estimating-digital-emissions/)
37
+ - 🏷️ **Rates** each page A+ to F on the SWD digital carbon rating scale
38
+ - 🚦 **Enforces** a budget: exits non-zero when a page goes over, so CI fails
39
+ - 💡 **Suggests** fixes: oversized images, legacy font formats, autoplaying media, heavy JS
40
+ - 📦 **No dependencies**: pure Python standard library
41
+
42
+ ## Install
43
+
44
+ ```bash
45
+ pip install greenpage
46
+ # or from source
47
+ pip install git+https://github.com/Amxr-tech/greenpage
48
+ ```
49
+
50
+ ## Usage
51
+
52
+ ```bash
53
+ # A whole static-site build
54
+ greenpage scan ./dist
55
+
56
+ # A single live page
57
+ greenpage scan https://example.com
58
+
59
+ # Fail if any page emits more than 0.2 g CO2e per view
60
+ greenpage scan ./dist --budget 0.2
61
+
62
+ # Site runs on renewable energy, 50k views/month: project yearly impact
63
+ greenpage scan ./dist --green-host --views 50000
64
+
65
+ # Machine-readable or PR-comment-ready output
66
+ greenpage scan ./dist --format json
67
+ greenpage scan ./dist --format markdown
68
+ ```
69
+
70
+ Example output:
71
+
72
+ ```
73
+ index.html
74
+ 150.5 kB transferred · 0.022 g CO2e/view · rating A+
75
+ ≈ 2.68 kg CO2e/year at 10,000 views/month
76
+ Heaviest resources:
77
+ 150.0 kB image assets/hero.png
78
+ 280 B document index.html
79
+ → assets/hero.png is 150 kB; serving it as AVIF or WebP typically saves 30-50%.
80
+ ```
81
+
82
+ | Exit code | Meaning |
83
+ |---|---|
84
+ | `0` | All pages are within budget (or no budget set) |
85
+ | `1` | At least one page is over budget |
86
+ | `2` | Usage error, e.g. the target was not found |
87
+
88
+ ## GitHub Action
89
+
90
+ ```yaml
91
+ - uses: actions/checkout@v4
92
+ - run: npm ci && npm run build
93
+ - uses: Amxr-tech/greenpage@v0
94
+ with:
95
+ target: dist
96
+ budget: "0.2"
97
+ ```
98
+
99
+ The action writes a Markdown report to the job summary and fails the job when a page is over budget.
100
+
101
+ ## Python API
102
+
103
+ ```python
104
+ from greenpage import estimate, scan_directory
105
+
106
+ for page in scan_directory("dist"):
107
+ e = estimate(page.total_bytes, green_hosting=True)
108
+ print(page.page, f"{e.grams_co2e:.3f} g", e.rating)
109
+ ```
110
+
111
+ ## Methodology
112
+
113
+ greenpage implements the **Sustainable Web Design Model v4**:
114
+
115
+ 1. **Bytes.** Every resource a page references (HTML, CSS, JS, images, fonts, media, iframes) is counted.
116
+ Text assets are measured gzip-compressed, which is how servers deliver them.
117
+ 2. **Energy.** Bytes are converted to energy at 0.194 kWh/GB operational and 0.106 kWh/GB embodied,
118
+ split across data centres, networks and user devices.
119
+ 3. **Carbon.** Energy is multiplied by grid intensity: 494 gCO₂e/kWh global average by default, configurable
120
+ with `--grid-intensity`. Embodied energy always uses the global average.
121
+ 4. **Green hosting.** Data-centre operational emissions are multiplied by `1 - green hosting factor`.
122
+ `--green-host` sets the factor to 1, as SWD v4 prescribes for hosts verified by the
123
+ [Green Web Foundation](https://www.thegreenwebfoundation.org/). The Python API also accepts a partial
124
+ `green_hosting_factor` (0–1).
125
+
126
+ All constants live in [`src/greenpage/model.py`](src/greenpage/model.py).
127
+
128
+ **Limitations.** These are estimates, not measurements. Local mode doesn't count resources loaded
129
+ at runtime by JavaScript, or external URLs (it flags them instead). The model assumes a first-time
130
+ visit with no caching. Use the numbers to compare pages and spot regressions, not as audited figures.
131
+
132
+ ## Roadmap
133
+
134
+ - [ ] Count CSS `url()` references (background images, `@font-face`)
135
+ - [ ] Automatic green-host lookup via the Green Web Foundation API
136
+ - [ ] Headless-browser mode to capture runtime-loaded resources
137
+ - [ ] Per-country grid intensity presets
138
+ - [ ] Track history and chart trends across commits
139
+
140
+ Want to work on one of these? See [CONTRIBUTING.md](CONTRIBUTING.md).
141
+
142
+ ## License
143
+
144
+ [MIT](LICENSE)
@@ -0,0 +1,124 @@
1
+ # 🌱 greenpage
2
+
3
+ **Estimate and budget the carbon footprint of web pages, locally or in CI.**
4
+
5
+ [![CI](https://github.com/Amxr-tech/greenpage/actions/workflows/ci.yml/badge.svg)](https://github.com/Amxr-tech/greenpage/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
7
+ ![Python](https://img.shields.io/badge/python-3.10%2B-blue)
8
+ ![Dependencies](https://img.shields.io/badge/dependencies-0-brightgreen)
9
+
10
+ The internet accounts for an estimated 3–4% of global greenhouse gas emissions,
11
+ about the same as the aviation industry. Every byte a page sends uses energy in data
12
+ centres, networks and on the visitor's device. Teams already set **performance budgets**;
13
+ greenpage adds a **carbon budget** that stops pages from quietly getting heavier.
14
+
15
+ - 📏 **Measures** every page in a static build (`dist/`, `public/`, `_site/`) offline, or any live URL
16
+ - 🧮 **Estimates** grams of CO₂e per page view using the open [Sustainable Web Design Model v4](https://sustainablewebdesign.org/estimating-digital-emissions/)
17
+ - 🏷️ **Rates** each page A+ to F on the SWD digital carbon rating scale
18
+ - 🚦 **Enforces** a budget: exits non-zero when a page goes over, so CI fails
19
+ - 💡 **Suggests** fixes: oversized images, legacy font formats, autoplaying media, heavy JS
20
+ - 📦 **No dependencies**: pure Python standard library
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install greenpage
26
+ # or from source
27
+ pip install git+https://github.com/Amxr-tech/greenpage
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ```bash
33
+ # A whole static-site build
34
+ greenpage scan ./dist
35
+
36
+ # A single live page
37
+ greenpage scan https://example.com
38
+
39
+ # Fail if any page emits more than 0.2 g CO2e per view
40
+ greenpage scan ./dist --budget 0.2
41
+
42
+ # Site runs on renewable energy, 50k views/month: project yearly impact
43
+ greenpage scan ./dist --green-host --views 50000
44
+
45
+ # Machine-readable or PR-comment-ready output
46
+ greenpage scan ./dist --format json
47
+ greenpage scan ./dist --format markdown
48
+ ```
49
+
50
+ Example output:
51
+
52
+ ```
53
+ index.html
54
+ 150.5 kB transferred · 0.022 g CO2e/view · rating A+
55
+ ≈ 2.68 kg CO2e/year at 10,000 views/month
56
+ Heaviest resources:
57
+ 150.0 kB image assets/hero.png
58
+ 280 B document index.html
59
+ → assets/hero.png is 150 kB; serving it as AVIF or WebP typically saves 30-50%.
60
+ ```
61
+
62
+ | Exit code | Meaning |
63
+ |---|---|
64
+ | `0` | All pages are within budget (or no budget set) |
65
+ | `1` | At least one page is over budget |
66
+ | `2` | Usage error, e.g. the target was not found |
67
+
68
+ ## GitHub Action
69
+
70
+ ```yaml
71
+ - uses: actions/checkout@v4
72
+ - run: npm ci && npm run build
73
+ - uses: Amxr-tech/greenpage@v0
74
+ with:
75
+ target: dist
76
+ budget: "0.2"
77
+ ```
78
+
79
+ The action writes a Markdown report to the job summary and fails the job when a page is over budget.
80
+
81
+ ## Python API
82
+
83
+ ```python
84
+ from greenpage import estimate, scan_directory
85
+
86
+ for page in scan_directory("dist"):
87
+ e = estimate(page.total_bytes, green_hosting=True)
88
+ print(page.page, f"{e.grams_co2e:.3f} g", e.rating)
89
+ ```
90
+
91
+ ## Methodology
92
+
93
+ greenpage implements the **Sustainable Web Design Model v4**:
94
+
95
+ 1. **Bytes.** Every resource a page references (HTML, CSS, JS, images, fonts, media, iframes) is counted.
96
+ Text assets are measured gzip-compressed, which is how servers deliver them.
97
+ 2. **Energy.** Bytes are converted to energy at 0.194 kWh/GB operational and 0.106 kWh/GB embodied,
98
+ split across data centres, networks and user devices.
99
+ 3. **Carbon.** Energy is multiplied by grid intensity: 494 gCO₂e/kWh global average by default, configurable
100
+ with `--grid-intensity`. Embodied energy always uses the global average.
101
+ 4. **Green hosting.** Data-centre operational emissions are multiplied by `1 - green hosting factor`.
102
+ `--green-host` sets the factor to 1, as SWD v4 prescribes for hosts verified by the
103
+ [Green Web Foundation](https://www.thegreenwebfoundation.org/). The Python API also accepts a partial
104
+ `green_hosting_factor` (0–1).
105
+
106
+ All constants live in [`src/greenpage/model.py`](src/greenpage/model.py).
107
+
108
+ **Limitations.** These are estimates, not measurements. Local mode doesn't count resources loaded
109
+ at runtime by JavaScript, or external URLs (it flags them instead). The model assumes a first-time
110
+ visit with no caching. Use the numbers to compare pages and spot regressions, not as audited figures.
111
+
112
+ ## Roadmap
113
+
114
+ - [ ] Count CSS `url()` references (background images, `@font-face`)
115
+ - [ ] Automatic green-host lookup via the Green Web Foundation API
116
+ - [ ] Headless-browser mode to capture runtime-loaded resources
117
+ - [ ] Per-country grid intensity presets
118
+ - [ ] Track history and chart trends across commits
119
+
120
+ Want to work on one of these? See [CONTRIBUTING.md](CONTRIBUTING.md).
121
+
122
+ ## License
123
+
124
+ [MIT](LICENSE)
@@ -0,0 +1,37 @@
1
+ name: greenpage carbon budget
2
+ description: Estimate the carbon footprint of your built site and fail when pages exceed a budget.
3
+ branding:
4
+ icon: feather
5
+ color: green
6
+ inputs:
7
+ target:
8
+ description: Build directory, HTML file, or URL to scan
9
+ required: true
10
+ budget:
11
+ description: Maximum grams CO2e per page view
12
+ required: false
13
+ default: ""
14
+ green-host:
15
+ description: Set to "true" if the site is hosted on renewable energy
16
+ required: false
17
+ default: "false"
18
+ runs:
19
+ using: composite
20
+ steps:
21
+ - uses: actions/setup-python@v5
22
+ with:
23
+ python-version: "3.12"
24
+ - shell: bash
25
+ run: pip install --quiet "$GITHUB_ACTION_PATH"
26
+ - shell: bash
27
+ env:
28
+ TARGET: ${{ inputs.target }}
29
+ BUDGET: ${{ inputs.budget }}
30
+ GREEN_HOST: ${{ inputs.green-host }}
31
+ run: |
32
+ args=(scan "$TARGET" --format markdown)
33
+ [ -n "$BUDGET" ] && args+=(--budget "$BUDGET")
34
+ [ "$GREEN_HOST" = "true" ] && args+=(--green-host)
35
+ set +e
36
+ greenpage "${args[@]}" | tee -a "$GITHUB_STEP_SUMMARY"
37
+ exit "${PIPESTATUS[0]}"
@@ -0,0 +1,41 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "greenpage"
7
+ dynamic = ["version"]
8
+ description = "Estimate and budget the carbon footprint of web pages, locally or in CI."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.10"
12
+ authors = [{ name = "greenpage contributors" }]
13
+ keywords = ["sustainability", "carbon", "green-software", "web-performance", "ci"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Environment :: Console",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Internet :: WWW/HTTP :: Site Management",
20
+ "Topic :: Software Development :: Quality Assurance",
21
+ ]
22
+ dependencies = []
23
+
24
+ [project.optional-dependencies]
25
+ dev = ["pytest>=8", "ruff>=0.6"]
26
+
27
+ [project.scripts]
28
+ greenpage = "greenpage.cli:main"
29
+
30
+ [tool.hatch.version]
31
+ path = "src/greenpage/__init__.py"
32
+
33
+ [tool.ruff]
34
+ line-length = 120
35
+ target-version = "py310"
36
+
37
+ [tool.ruff.lint]
38
+ select = ["E", "F", "I", "B", "UP"]
39
+
40
+ [tool.pytest.ini_options]
41
+ testpaths = ["tests"]
@@ -0,0 +1,11 @@
1
+ """greenpage: estimate and budget the carbon footprint of web pages."""
2
+
3
+ __version__ = "0.1.1"
4
+
5
+ from .model import Estimate, estimate, rating_for # noqa: E402
6
+ from .scan import PageScan, Resource, scan_directory, scan_file, scan_url # noqa: E402
7
+
8
+ __all__ = [
9
+ "Estimate", "PageScan", "Resource", "estimate", "rating_for",
10
+ "scan_directory", "scan_file", "scan_url", "__version__",
11
+ ]
@@ -0,0 +1,5 @@
1
+ import sys
2
+
3
+ from .cli import main
4
+
5
+ sys.exit(main())
@@ -0,0 +1,34 @@
1
+ """Actionable suggestions for reducing a page's footprint."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import PurePosixPath
6
+ from urllib.parse import urlparse
7
+
8
+ from .scan import PageScan
9
+
10
+ KB = 1000
11
+
12
+
13
+ def suggestions(scan: PageScan) -> list[str]:
14
+ tips: list[str] = []
15
+ by_kind: dict[str, int] = {}
16
+ for r in scan.resources:
17
+ by_kind[r.kind] = by_kind.get(r.kind, 0) + r.bytes
18
+
19
+ for r in scan.resources:
20
+ ext = PurePosixPath(urlparse(r.url).path).suffix.lower()
21
+ if r.kind == "image" and ext in {".png", ".jpg", ".jpeg", ".gif"} and r.bytes > 100 * KB:
22
+ tips.append(f"{r.url} is {r.bytes // KB} kB; serving it as AVIF or WebP typically saves 30-50%.")
23
+ elif r.kind == "font" and ext in {".ttf", ".otf"}:
24
+ tips.append(f"{r.url} uses {ext}; WOFF2 is much smaller and universally supported.")
25
+ elif r.kind == "media" and r.bytes > 1000 * KB:
26
+ tips.append(f"{r.url} is {r.bytes // KB} kB; use preload=\"none\" or a poster so it only loads on demand.")
27
+
28
+ if by_kind.get("script", 0) > 300 * KB:
29
+ tips.append(
30
+ f"JavaScript totals {by_kind['script'] // KB} kB; consider code-splitting or removing unused libraries."
31
+ )
32
+ if by_kind.get("font", 0) > 150 * KB:
33
+ tips.append(f"Web fonts total {by_kind['font'] // KB} kB; subset them or prefer system fonts.")
34
+ return tips