This is the foundation for #7329. Every renderer is blocked by it.
1. A media route:GET /:owner/:name/media/:ref/*path. It serves a file's bytes with a real, allowlisted content type, so the browser can show them in <img>, <video>, <audio>, <object> and fetch():
Allowed types: png, jpeg, gif, webp, avif, bmp, ico, svg (image/svg+xml), pdf, mp3, ogg/oga, wav, flac, m4a/aac, mp4/m4v, webm, mov, plus model/stl and model/gltf-binary. These are decided by extension and confirmed by magic bytes where a format has them. A mismatch falls back to application/octet-stream.
Never text/html or JavaScript. Send X-Content-Type-Options: nosniff and Content-Security-Policy: sandbox; default-src 'none'; img-src 'self' data:; media-src 'self'; style-src 'unsafe-inline', so an SVG with a <script> renders as an image and never runs.
HTTP Range: answer 206 with Accept-Ranges: bytes, so video and audio can seek and a PDF can stream. Test a single range, a suffix range and an unsatisfiable range (416).
Caching: an ETag of the blob id, and Cache-Control: private, max-age=31536000, immutable when :ref is a full commit sha, otherwise short.
Access is the same as /raw. A private repo stays private, and the route is tested signed out.
Big files stream from git or the bucket, and are never buffered whole. Cap per kind, and over the cap the page offers Download.
2. One registry of file kinds:src/web/render/kinds.ts. fileKind(path, firstBytes) returns image | svg | pdf | audio | video | csv | model3d | config | markdown | text | binary. Each kind maps to a renderer module that a later issue fills in.
renderBlob asks the registry. A kind with no renderer yet keeps today's view, so nothing regresses.
The blob page header gets the switchRendered | Source | Raw | Download, generalising Markdown's ?view=source.
Tests:
Content type per extension, and a magic-byte mismatch falls back.
An SVG with a script is served under the sandbox CSP.
206 ranges and the 416 case.
A private repo returns 404 signed out.
A 304 on the ETag.
The registry maps every listed extension.
Shared rules for every issue in this epic (#7329):
Each renderer lives in its own file under src/web/render/<kind>.ts and registers itself in the kind registry from #7330. Don't grow src/web/pages/repo.ts. It's a hot file, and five open pulls on it stall the crew.
No CDN and no remote scripts (the forge CSP). Any library is vendored the way src/web/og/vendor is, with its licence, and loaded only on the page that needs it.
Repo content never runs as script on the forge origin. Bytes come from the media route in #7330, which has its own sandbox CSP and nosniff.
Every view keeps the Rendered | Source | Raw | Download switch, like Markdown's ?view=source, so nothing is lost.
It works at phone width (390 px) and in both themes. Tests cover the HTML shape and the bytes route, not pixels.
It stays fast: the blob page's existing budget holds. Heavy viewers load their script lazily.
This is the foundation for #7329. Every renderer is blocked by it.
1. A media route:
GET /:owner/:name/media/:ref/*path. It serves a file's bytes with a real, allowlisted content type, so the browser can show them in<img>,<video>,<audio>,<object>andfetch():model/stlandmodel/gltf-binary. These are decided by extension and confirmed by magic bytes where a format has them. A mismatch falls back toapplication/octet-stream.text/htmlor JavaScript. SendX-Content-Type-Options: nosniffandContent-Security-Policy: sandbox; default-src 'none'; img-src 'self' data:; media-src 'self'; style-src 'unsafe-inline', so an SVG with a<script>renders as an image and never runs.Accept-Ranges: bytes, so video and audio can seek and a PDF can stream. Test a single range, a suffix range and an unsatisfiable range (416).ETagof the blob id, andCache-Control: private, max-age=31536000, immutablewhen:refis a full commit sha, otherwise short./raw. A private repo stays private, and the route is tested signed out.2. One registry of file kinds:
src/web/render/kinds.ts.fileKind(path, firstBytes)returnsimage | svg | pdf | audio | video | csv | model3d | config | markdown | text | binary. Each kind maps to a renderer module that a later issue fills in.renderBlobasks the registry. A kind with no renderer yet keeps today's view, so nothing regresses.Rendered | Source | Raw | Download, generalising Markdown's?view=source.Tests:
Shared rules for every issue in this epic (#7329):
src/web/render/<kind>.tsand registers itself in the kind registry from #7330. Don't growsrc/web/pages/repo.ts. It's a hot file, and five open pulls on it stall the crew.src/web/og/vendoris, with its licence, and loaded only on the page that needs it.nosniff.Rendered | Source | Raw | Downloadswitch, like Markdown's?view=source, so nothing is lost.