Skip to content

Commit 3405a78

Browse files
IamAlex-afkclaude
andcommitted
Harden CSP, add GitHub meta files, fix a11y/SEO gaps, optimize cover image
CSP: add object-src none, base-uri none, upgrade-insecure-requests. SEO: shorten title/meta description to fit SERP limits on all 4 languages; add cover-small.jpg/webp (624x630->624x328) for inline About block instead of reusing the 1200x630 og:image, cutting ~12-16KB per page load. A11y: role=radiogroup/aria-checked on quiz options, replace innerHTML with textContent for cta-footer, replace alert() with an accessible toast, respect prefers-reduced-motion, add a print stylesheet. GitHub hygiene: CODEOWNERS, Dependabot, CodeQL workflow. Docs: SECURITY.md contact + platform limitations, manifest.json categories, ai.txt, 404.html SEO tags, privacy/terms OG tags + hreflang. Cleanup: remove dead [data-lang] CSS rules now that LangFilter strips non-matching languages at build time. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 11954b0 commit 3405a78

80 files changed

Lines changed: 407 additions & 155 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/CODEOWNERS

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Default owner for everything in the repo, unless overridden below.
2+
* sintyau1777@gmail.com

.github/dependabot.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
version: 2
2+
updates:
3+
- package-ecosystem: "github-actions"
4+
directory: "/"
5+
schedule:
6+
interval: "weekly"

.github/workflows/codeql.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: "CodeQL"
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
schedule:
9+
- cron: '0 6 * * 1' # weekly, Monday 06:00 UTC
10+
11+
jobs:
12+
analyze:
13+
name: Analyze JavaScript
14+
runs-on: ubuntu-latest
15+
permissions:
16+
contents: read
17+
security-events: write
18+
19+
steps:
20+
- name: Checkout repository
21+
uses: actions/checkout@v4
22+
23+
- name: Initialize CodeQL
24+
uses: github/codeql-action/init@v3
25+
with:
26+
languages: javascript
27+
28+
- name: Perform CodeQL Analysis
29+
uses: github/codeql-action/analyze@v3
30+
with:
31+
category: "/language:javascript"

.well-known/security.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
Contact: https://t.me/ASTROSCAN_BOT
2+
Contact: mailto:sintyau1777@gmail.com
23
Expires: 2027-06-22T00:00:00.000Z
34
Preferred-Languages: en, ru, es, hi
45
Canonical: https://astroscan-lab.github.io/astroscan_bot/.well-known/security.txt

404.html

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55
<meta name="viewport" content="width=device-width, initial-scale=1.0">
66
<meta name="robots" content="noindex">
77
<title>404 · Page Not Found — AstroScan</title>
8+
<meta name="description" content="The page you're looking for doesn't exist. Take the free AstroScan personality test or return to the homepage.">
9+
<meta property="og:title" content="404 · Page Not Found — AstroScan">
10+
<meta property="og:description" content="The page you're looking for doesn't exist. Take the free AstroScan personality test or return to the homepage.">
11+
<meta property="og:image" content="https://astroscan-lab.github.io/astroscan_bot/cover.jpg">
12+
<meta property="og:url" content="https://astroscan-lab.github.io/astroscan_bot/404.html">
13+
<meta property="og:type" content="website">
814
<link rel="icon" type="image/x-icon" href="/astroscan_bot/favicon.ico">
915
<style>
1016
* { margin: 0; padding: 0; box-sizing: border-box; }

SECURITY.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ AstroScan is a static, client-side personality test (no backend, no account syst
77
If you find a security issue (XSS, CSP bypass, dependency confusion, etc.), please report it privately via:
88

99
- **Telegram:** [@ASTROSCAN_BOT](https://t.me/ASTROSCAN_BOT)
10+
- **Email:** sintyau1777@gmail.com
1011

1112
Please do not open a public GitHub issue for security reports. We aim to acknowledge reports within 7 days.
1213

1314
## Scope
1415

15-
- `index.html`, `ru/`, `es/`, `hi/` — the static frontend
16-
- `app.js`-style inline scripts and the Content-Security-Policy configuration
16+
- `template.html`, `privacy_template.html`, `terms_template.html` and the pages `build.py`/`build_zodiac.py` generate from them (`index.html`, `ru/`, `es/`, `hi/`, zodiac sign pages, `privacy.html`, `terms.html`)
17+
- `assets/js/main.js` and the Content-Security-Policy configuration
1718

1819
Out of scope: the Telegram bot backend and its natal-chart calculation pipeline, which are not part of this repository.
20+
21+
## Known Platform Limitations
22+
23+
This site is hosted on GitHub Pages' free tier, which does not allow setting custom HTTP response headers. As a result:
24+
25+
- **`X-Frame-Options`** cannot be set at the HTTP level. Clickjacking protection is instead enforced via the `frame-ancestors` directive in the CSP `<meta>` tag, which all modern browsers honor (note: `frame-ancestors` has no effect when delivered via `<meta>` per spec in some older browsers — this is a known tradeoff of static hosting without server-side header control).
26+
- **HSTS** cannot be set at all — it is an HTTP-header-only mechanism and has no `<meta>` equivalent. GitHub Pages itself serves over HTTPS with redirect, which provides baseline protection, but we cannot enforce HSTS preloading from this repository.
27+
28+
If these matter for your use case, hosting behind a CDN/reverse proxy that adds these headers (e.g. Cloudflare) would be required — that's a deployment change outside this repository's scope.

ai.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# ai.txt — experimental, not yet a ratified standard (unlike robots.txt).
2+
# Included as a forward-looking complement to llms.txt; AI crawlers/assistants
3+
# should treat llms.txt as the authoritative structured summary of this site.
4+
LLM: https://astroscan-lab.github.io/astroscan_bot/llms.txt
5+
Allow: /

assets/css/styles.css

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,6 @@
8484
.cta-bar a { flex: 1; text-align: center; padding: 14px; text-decoration: none; font-weight: bold; text-transform: uppercase; color: var(--text-light); }
8585
.cta-bar .telegram-cta { background: var(--accent-primary); color: #0a0718; }
8686
.lazy-section { min-height: 300px; contain: layout; }
87-
[data-lang] { display: none; }
88-
body.lang-en [data-lang="en"] { display: revert; }
89-
body.lang-ru [data-lang="ru"] { display: revert; }
90-
body.lang-es [data-lang="es"] { display: revert; }
91-
body.lang-hi [data-lang="hi"] { display: revert; }
9287
*:focus-visible { outline: 2px solid var(--accent-primary); outline-offset: 2px; }
9388
@media (max-width: 700px) { body { padding: 1rem; } .static-hero h1 { font-size: 1.8rem; } .card { padding: 1rem; }.static-hero { min-height: 500px; } }
9489
.hidden { display: none; }
@@ -114,3 +109,33 @@
114109

115110
.service-card-link { text-decoration: none; border: 1px solid var(--border-dim); border-radius: 1rem; padding: 1rem; display: block; color: inherit; transition: border-color 0.2s; }
116111
.service-card-link:hover { border-color: var(--accent-primary); }
112+
113+
@media (prefers-reduced-motion: reduce) {
114+
*, *::before, *::after {
115+
animation-duration: 0.01ms !important;
116+
animation-iteration-count: 1 !important;
117+
transition-duration: 0.01ms !important;
118+
}
119+
.telegram-cta { animation: none !important; }
120+
.fade-up { opacity: 1 !important; transform: none !important; }
121+
}
122+
123+
@media print {
124+
body { background: white !important; color: black !important; padding: 0 !important; }
125+
.cta-bar, .lang-bar, .telegram-link, .telegram-cta, .skip-link { display: none !important; }
126+
.card { border: 1px solid #ccc !important; box-shadow: none !important; break-inside: avoid; }
127+
.static-hero h1 { color: black !important; background: none !important; -webkit-background-clip: initial !important; background-clip: initial !important; }
128+
a { color: #000 !important; text-decoration: underline !important; }
129+
}
130+
131+
.toast-container {
132+
position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%);
133+
z-index: 1000; pointer-events: none;
134+
}
135+
.toast {
136+
background: var(--card-bg); color: var(--text-light); padding: 0.75rem 1.5rem;
137+
border-radius: 1.5rem; border: 1px solid var(--accent-primary); margin-top: 0.5rem;
138+
opacity: 0; transform: translateY(20px); transition: opacity 0.3s ease, transform 0.3s ease;
139+
pointer-events: auto; max-width: 90vw; text-align: center;
140+
}
141+
.toast.visible { opacity: 1; transform: translateY(0); }

assets/js/main.js

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,28 @@
9797
const LANGS = ['en','ru','hi','es'];
9898
const LANG_NAMES = { en:'English', ru:'Русский', hi:'हिन्दी', es:'Español' };
9999
let currentLang = 'en';
100+
function showToast(message, duration) {
101+
duration = duration || 3000;
102+
let container = document.querySelector('.toast-container');
103+
if (!container) {
104+
container = document.createElement('div');
105+
container.className = 'toast-container';
106+
container.setAttribute('role', 'region');
107+
container.setAttribute('aria-label', 'Notifications');
108+
document.body.appendChild(container);
109+
}
110+
const toast = document.createElement('div');
111+
toast.className = 'toast';
112+
toast.setAttribute('role', 'alert');
113+
toast.setAttribute('aria-live', 'polite');
114+
toast.textContent = message;
115+
container.appendChild(toast);
116+
requestAnimationFrame(() => requestAnimationFrame(() => toast.classList.add('visible')));
117+
setTimeout(() => {
118+
toast.classList.remove('visible');
119+
setTimeout(() => toast.remove(), 300);
120+
}, duration);
121+
}
100122
const translations = {
101123
en: {
102124
ctaFooter: '⚡ GET FULL REPORT IN TELEGRAM',
@@ -332,7 +354,7 @@
332354
document.querySelectorAll('.lang-btn').forEach(b => b.classList.remove('active'));
333355
document.getElementById(`lang-${lang}`)?.classList.add('active');
334356
const t = translations[lang];
335-
document.getElementById('cta-footer').innerHTML = t.ctaFooter;
357+
document.getElementById('cta-footer').textContent = t.ctaFooter;
336358
if (!window.currentQuizState) renderApp();
337359
else if (window.currentQuizState === 'quiz') renderApp();
338360
else if (window.currentQuizState === 'result') renderResult();
@@ -442,18 +464,19 @@
442464
const t = translations[currentLang];
443465
const questionText = q[`text${currentLang.charAt(0).toUpperCase()+currentLang.slice(1)}`] || q.textEn;
444466
const options = q[`options${currentLang.charAt(0).toUpperCase()+currentLang.slice(1)}`] || q.optionsEn;
445-
const optsHtml = options.map((opt, idx) => `<button class="option-btn" data-opt="${idx}" type="button">${opt}</button>`).join('');
467+
const optsHtml = options.map((opt, idx) => `<button class="option-btn" data-opt="${idx}" type="button" role="radio" aria-checked="false">${opt}</button>`).join('');
446468
app.innerHTML = `
447469
<div class="card">
448470
<div class="progress-bar"><div class="progress-fill" style="width:${(currentQuestion/QUESTIONS.length)*100}%"></div></div>
449-
<div class="question-text">${questionText}</div>
450-
<div class="options">${optsHtml}</div>
471+
<div class="question-text" id="questionLabel">${questionText}</div>
472+
<div class="options" role="radiogroup" aria-labelledby="questionLabel">${optsHtml}</div>
451473
<button class="btn-primary" id="nextBtn">${currentQuestion === QUESTIONS.length-1 ? t.result : t.next}</button>
452474
</div>`;
453475
document.querySelectorAll('.option-btn').forEach(btn => {
454476
btn.addEventListener('click', () => {
455-
document.querySelectorAll('.option-btn').forEach(b => b.classList.remove('selected'));
477+
document.querySelectorAll('.option-btn').forEach(b => { b.classList.remove('selected'); b.setAttribute('aria-checked', 'false'); });
456478
btn.classList.add('selected');
479+
btn.setAttribute('aria-checked', 'true');
457480
answers[currentQuestion] = parseInt(btn.dataset.opt);
458481
});
459482
});
@@ -568,7 +591,7 @@
568591
setTimeout(() => URL.revokeObjectURL(url), 1000);
569592
} catch (e) {
570593
if (navigator.share) { navigator.share({ title: 'AstroScan Result', url: window.location.href }); }
571-
else { alert(t.copyLink); }
594+
else { showToast(t.copyLink); }
572595
}
573596
});
574597
function getTrackerData() {

build.ps1

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Local build script for AstroScan static site.
2+
# Mirrors .github/workflows/build.yml: regenerate all pages from the templates,
3+
# regenerate the sitemap, then validate everything before you commit.
4+
#
5+
# Usage: .\build.ps1
6+
7+
$ErrorActionPreference = "Stop"
8+
9+
function Run-Step($Name, $Command) {
10+
Write-Host "==> $Name" -ForegroundColor Cyan
11+
& python @Command
12+
if ($LASTEXITCODE -ne 0) {
13+
Write-Host "FAILED: $Name (exit code $LASTEXITCODE)" -ForegroundColor Red
14+
exit $LASTEXITCODE
15+
}
16+
}
17+
18+
Run-Step "Generate index.html / ru / es / hi / privacy / terms from templates" @("build.py")
19+
Run-Step "Generate zodiac sign pages (12 signs x 4 languages)" @("build_zodiac.py")
20+
Run-Step "Regenerate sitemap.xml" @("scripts/gen_sitemap.py")
21+
Run-Step "Validate build output" @("validate.py")
22+
23+
Write-Host "Build OK." -ForegroundColor Green

0 commit comments

Comments
 (0)