Fix missing recipe images and validate complete Pages bundle
Sekta Engineer committed 2026-09-08 14:10:01 +0000

Analyzing diff...

.oxygit/ci.yml+5 −0
index 73ddebc..a06bdb5 100644
--- a/.oxygit/ci.yml
+++ b/.oxygit/ci.yml
@@ -6,5 +6,10 @@ publish_pages:
   script:
     - mkdir -p public
     - 'cp "Курс для беременных 2026/Маркетинговые материалы/recepty---25a9d15c-9cda-432f-9444-d94255eb206e.html" public/index.html'
+    - 'cp "Курс для беременных 2026/Маркетинговые материалы/"recipe-*.png public/'
+    - python3 scripts/validate_static.py public --manifest static-manifest.json
+  artifacts:
+    paths: [static-manifest.json]
+    expire_in: never
   pages:
     path: public
scripts/validate_static.py+55 −0
new file mode 100644
index 0000000..260846a
--- /dev/null
+++ b/scripts/validate_static.py
@@ -0,0 +1,55 @@
+#!/usr/bin/env python3
+"""Validate a static bundle without network access; emit a reproducible byte manifest."""
+import argparse, hashlib, json, re, sys
+from html.parser import HTMLParser
+from pathlib import Path
+from urllib.parse import unquote, urlsplit
+
+class References(HTMLParser):
+    def __init__(self):
+        super().__init__(); self.refs=[]
+    def handle_starttag(self, tag, attrs):
+        a=dict(attrs)
+        for k in ('src','poster'):
+            if a.get(k): self.refs.append(a[k])
+        if tag=='link' and a.get('href'): self.refs.append(a['href'])
+        if a.get('srcset') and not a['srcset'].startswith('data:'):
+            self.refs.extend(x.strip().split()[0] for x in a['srcset'].split(',') if x.strip())
+        if a.get('style'): self.refs.extend(css_refs(a['style']))
+
+def css_refs(s):
+    return re.findall(r'url\(\s*[\"\']?([^\"\')]+)',s)+re.findall(r'@import\s+[\"\']([^\"\']+)',s)
+
+def validate(root):
+    root=root.resolve(); errors=[]; files=[]
+    for p in sorted(root.rglob('*')):
+        if p.is_symlink(): errors.append(f'symlink: {p.relative_to(root)}'); continue
+        if not p.is_file(): continue
+        rel=p.relative_to(root).as_posix(); data=p.read_bytes()
+        if p.suffix.lower()=='.png' and not data.startswith(b'\x89PNG\r\n\x1a\n'):
+            errors.append(f'not PNG: {rel}')
+        if p.suffix.lower() in ('.js','.css','.png','.jpg','.jpeg','.webp','.pdf') and data.lstrip().lower().startswith((b'<!doctype html',b'<html')):
+            errors.append(f'HTML masquerading as asset: {rel}')
+        refs=[]
+        if p.suffix.lower() in ('.html','.htm'):
+            parser=References(); txt=data.decode('utf-8'); parser.feed(txt); refs=parser.refs+css_refs(txt)
+        elif p.suffix.lower()=='.css': refs=css_refs(data.decode('utf-8'))
+        for ref in refs:
+            u=urlsplit(ref.strip())
+            if u.scheme or u.netloc or not u.path: continue
+            # Root-relative routes belong to the serving origin, not this bundle.
+            if u.path.startswith('/'): errors.append(f'root-relative resource needs explicit mapping: {rel}: {ref}'); continue
+            target=(p.parent/unquote(u.path)).resolve()
+            if not target.is_relative_to(root): errors.append(f'path escapes bundle: {rel}: {ref}')
+            elif not target.is_file(): errors.append(f'missing resource: {rel}: {ref}')
+        files.append({'path':rel,'bytes':len(data),'sha256':hashlib.sha256(data).hexdigest()})
+    if not any(x['path'].endswith(('.html','.htm')) for x in files): errors.append('no HTML entry')
+    return files, sorted(set(errors))
+
+if __name__=='__main__':
+    p=argparse.ArgumentParser(); p.add_argument('root',type=Path); p.add_argument('--manifest',type=Path); a=p.parse_args()
+    files, errors=validate(a.root)
+    if errors:
+        print('\n'.join(errors),file=sys.stderr); sys.exit(1)
+    if a.manifest: a.manifest.write_text(json.dumps({'schema':1,'files':files},ensure_ascii=False,indent=2)+'\n')
+    print(f'Validated {len(files)} files')
diff --git "a/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-01-omelette.png" "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-01-omelette.png"+0 −0
new file mode 100644
index 0000000..f7fe0e2
Binary files /dev/null and "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-01-omelette.png" differ
diff --git "a/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-02-turkey.png" "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-02-turkey.png"+0 −0
new file mode 100644
index 0000000..dfbcf2a
Binary files /dev/null and "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-02-turkey.png" differ
diff --git "a/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-03-salmon.png" "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-03-salmon.png"+0 −0
new file mode 100644
index 0000000..d5a6a15
Binary files /dev/null and "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-03-salmon.png" differ
diff --git "a/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-04-cottage.png" "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-04-cottage.png"+0 −0
new file mode 100644
index 0000000..8ddf60b
Binary files /dev/null and "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-04-cottage.png" differ
diff --git "a/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-05-hummus.png" "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-05-hummus.png"+0 −0
new file mode 100644
index 0000000..d8f1a59
Binary files /dev/null and "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-05-hummus.png" differ
diff --git "a/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-06-toast.png" "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-06-toast.png"+0 −0
new file mode 100644
index 0000000..3ea6fdc
Binary files /dev/null and "b/\320\232\321\203\321\200\321\201 \320\264\320\273\321\217 \320\261\320\265\321\200\320\265\320\274\320\265\320\275\320\275\321\213\321\205 2026/\320\234\320\260\321\200\320\272\320\265\321\202\320\270\320\275\320\263\320\276\320\262\321\213\320\265 \320\274\320\260\321\202\320\265\321\200\320\270\320\260\320\273\321\213/recipe-06-toast.png" differ