Skip to content

Commit 439290f

Browse files
committed
Landing page: SEO, GA4 with consent, publish robots/sitemap
- Progressive-enhancement reveal, meta/OG, JSON-LD, intro h2 - Add robots.txt and sitemap.xml; publish_feeds copies them with index - Google Analytics (gtag), Consent Mode banner, file_download on download CTAs Made-with: Cursor
1 parent a73a3c4 commit 439290f

4 files changed

Lines changed: 191 additions & 6 deletions

File tree

gh-pages-root/index.html

Lines changed: 162 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,66 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8">
5+
<script>document.documentElement.classList.add('js-reveal');</script>
56
<meta name="viewport" content="width=device-width, initial-scale=1">
7+
<!-- Consent Mode defaults (before gtag.js); restored choice from localStorage if any -->
8+
<script>
9+
window.dataLayer = window.dataLayer || [];
10+
function gtag(){dataLayer.push(arguments);}
11+
(function () {
12+
var key = 'cuepoint_ga_consent';
13+
var stored = null;
14+
try { stored = localStorage.getItem(key); } catch (e) {}
15+
gtag('consent', 'default', {
16+
ad_storage: 'denied',
17+
ad_user_data: 'denied',
18+
ad_personalization: 'denied',
19+
analytics_storage: 'denied',
20+
wait_for_update: 500
21+
});
22+
if (stored === 'granted') {
23+
gtag('consent', 'update', {
24+
ad_storage: 'granted',
25+
ad_user_data: 'granted',
26+
ad_personalization: 'granted',
27+
analytics_storage: 'granted'
28+
});
29+
}
30+
})();
31+
</script>
32+
<script async src="https://www.googletagmanager.com/gtag/js?id=G-4QM1M6Q2ZT"></script>
33+
<script>
34+
gtag('js', new Date());
35+
gtag('config', 'G-4QM1M6Q2ZT');
36+
</script>
637
<title>CuePoint – Accurate metadata for Rekordbox</title>
738
<meta name="description" content="Match Rekordbox tracks to Beatport for key, BPM, label, genre, and release date. Desktop app for Windows and macOS with a full audit trail.">
39+
<meta name="robots" content="index, follow, max-image-preview:large">
40+
<meta name="theme-color" content="#0d1117">
841
<link rel="canonical" href="https://stuchain.github.io/CuePoint/">
942
<meta property="og:title" content="CuePoint – Accurate metadata for Rekordbox">
1043
<meta property="og:description" content="Match Rekordbox tracks to Beatport for key, BPM, label, genre, and release date. Desktop app for Windows and macOS with a full audit trail.">
1144
<meta property="og:url" content="https://stuchain.github.io/CuePoint/">
1245
<meta property="og:type" content="website">
46+
<meta property="og:site_name" content="CuePoint">
47+
<meta property="og:locale" content="en_US">
1348
<meta property="og:image" content="https://stuchain.github.io/CuePoint/logo.png">
49+
<meta property="og:image:width" content="120">
50+
<meta property="og:image:height" content="120">
51+
<meta property="og:image:alt" content="CuePoint logo">
1452
<meta name="twitter:card" content="summary">
1553
<meta name="twitter:title" content="CuePoint – Accurate metadata for Rekordbox">
1654
<meta name="twitter:description" content="Match Rekordbox tracks to Beatport for key, BPM, label, genre, and release date. Desktop app for Windows and macOS with a full audit trail.">
1755
<meta name="twitter:image" content="https://stuchain.github.io/CuePoint/logo.png">
1856
<link rel="icon" href="logo.png" type="image/png">
1957
<link rel="shortcut icon" href="logo.png" type="image/png">
2058
<link rel="apple-touch-icon" href="logo.png">
59+
<script type="application/ld+json">
60+
{"@context":"https://schema.org","@graph":[
61+
{"@type":"WebSite","name":"CuePoint","url":"https://stuchain.github.io/CuePoint/","description":"Match Rekordbox tracks to Beatport for key, BPM, label, genre, and release date. Desktop app for Windows and macOS with a full audit trail."},
62+
{"@type":"SoftwareApplication","name":"CuePoint","url":"https://stuchain.github.io/CuePoint/","applicationCategory":"DesktopApplication","operatingSystem":"Windows, macOS","offers":{"@type":"Offer","price":"0","priceCurrency":"USD"},"downloadUrl":"https://github.com/stuchain/CuePoint/releases/latest"}
63+
]}
64+
</script>
2165
<style>
2266
:root {
2367
--bg: #0d1117;
@@ -53,10 +97,23 @@
5397
/* Layout */
5498
.page { max-width: 960px; margin: 0 auto; padding: 0 1.5rem; }
5599
section { padding: 4rem 0; }
56-
section.reveal { opacity: 0; transform: translateY(24px); transition: opacity 0.6s ease, transform 0.6s ease; }
57-
section.reveal.visible { opacity: 1; transform: translateY(0); }
100+
html.js-reveal section.reveal,
101+
html.js-reveal footer.reveal {
102+
opacity: 0;
103+
transform: translateY(24px);
104+
transition: opacity 0.6s ease, transform 0.6s ease;
105+
}
106+
html.js-reveal section.reveal.visible,
107+
html.js-reveal footer.reveal.visible {
108+
opacity: 1;
109+
transform: translateY(0);
110+
}
58111
@media (prefers-reduced-motion: reduce) {
59-
section.reveal { transform: translateY(8px); transition: opacity 0.2s ease, transform 0.2s ease; }
112+
html.js-reveal section.reveal,
113+
html.js-reveal footer.reveal {
114+
transform: translateY(8px);
115+
transition: opacity 0.2s ease, transform 0.2s ease;
116+
}
60117
}
61118

62119
/* Hero */
@@ -146,6 +203,7 @@
146203
}
147204

148205
/* Intro */
206+
.intro .section-title { margin-bottom: 1rem; }
149207
.intro .lead {
150208
font-size: 1.25rem;
151209
color: var(--muted);
@@ -327,6 +385,48 @@
327385
margin: 0 auto;
328386
line-height: 1.5;
329387
}
388+
389+
/* Cookie / analytics consent (Consent Mode + EEA guidance) */
390+
.consent-banner {
391+
position: fixed;
392+
bottom: 0;
393+
left: 0;
394+
right: 0;
395+
z-index: 9999;
396+
background: var(--surface);
397+
border-top: 1px solid var(--border);
398+
padding: 1rem 1.25rem;
399+
box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.35);
400+
display: none;
401+
}
402+
.consent-banner.is-visible { display: block; }
403+
.consent-inner {
404+
max-width: 960px;
405+
margin: 0 auto;
406+
display: flex;
407+
flex-wrap: wrap;
408+
gap: 1rem;
409+
align-items: center;
410+
justify-content: space-between;
411+
}
412+
.consent-banner p { margin: 0; font-size: 0.875rem; color: var(--text); flex: 1 1 240px; line-height: 1.5; }
413+
.consent-actions {
414+
display: flex;
415+
flex-wrap: wrap;
416+
gap: 0.5rem;
417+
align-items: center;
418+
justify-content: flex-end;
419+
}
420+
.consent-actions a {
421+
color: var(--link);
422+
font-size: 0.8125rem;
423+
text-decoration: none;
424+
margin-right: 0.25rem;
425+
}
426+
.consent-actions a:hover { color: var(--link-hover); text-decoration: underline; }
427+
.consent-actions .btn { padding: 0.45rem 0.9rem; font-size: 0.8125rem; border: none; cursor: pointer; }
428+
.consent-actions .btn-secondary { background: var(--border); color: var(--text); }
429+
.consent-actions .btn-secondary:hover { background: #484f58; color: var(--text); }
330430
</style>
331431
</head>
332432
<body>
@@ -343,8 +443,9 @@ <h1>CuePoint</h1>
343443
</header>
344444

345445
<main>
346-
<section class="intro reveal">
446+
<section class="intro reveal" aria-labelledby="intro-heading">
347447
<div class="page">
448+
<h2 id="intro-heading" class="section-title">About CuePoint</h2>
348449
<p class="lead">Match your Rekordbox tracks to Beatport and get key, BPM, label, genre, and release date without manual lookup.</p>
349450
<p class="sub">Export from Rekordbox → run CuePoint → review or re-import. Full audit trail for every match.</p>
350451
<p class="tools-line">CuePoint includes <strong>inKey</strong> (Beatport metadata for Rekordbox playlists) and <strong>inCrate</strong> (inventory, charts, new releases, and Beatport playlists from your collection).</p>
@@ -413,6 +514,50 @@ <h2>Download</h2>
413514
</div>
414515
</footer>
415516

517+
<div id="consent-banner" class="consent-banner" role="dialog" aria-labelledby="consent-heading" aria-describedby="consent-desc" hidden>
518+
<div class="consent-inner">
519+
<p id="consent-desc"><strong id="consent-heading">Analytics</strong> — We use Google Analytics to understand traffic to this page. You can accept measurement cookies or decline; the site works either way.</p>
520+
<div class="consent-actions">
521+
<a href="https://github.com/stuchain/CuePoint/blob/main/PRIVACY_NOTICE.md" target="_blank" rel="noopener noreferrer">Privacy notice</a>
522+
<button type="button" class="btn btn-secondary" id="consent-decline">Decline</button>
523+
<button type="button" class="btn" id="consent-accept">Accept</button>
524+
</div>
525+
</div>
526+
</div>
527+
528+
<script>
529+
(function () {
530+
var key = 'cuepoint_ga_consent';
531+
var banner = document.getElementById('consent-banner');
532+
if (!banner) return;
533+
var stored = null;
534+
try { stored = localStorage.getItem(key); } catch (e) {}
535+
if (stored !== null) return;
536+
banner.removeAttribute('hidden');
537+
banner.classList.add('is-visible');
538+
function hide() {
539+
banner.classList.remove('is-visible');
540+
banner.setAttribute('hidden', '');
541+
}
542+
document.getElementById('consent-accept').addEventListener('click', function () {
543+
try { localStorage.setItem(key, 'granted'); } catch (e) {}
544+
if (typeof gtag === 'function') {
545+
gtag('consent', 'update', {
546+
ad_storage: 'granted',
547+
ad_user_data: 'granted',
548+
ad_personalization: 'granted',
549+
analytics_storage: 'granted'
550+
});
551+
}
552+
hide();
553+
});
554+
document.getElementById('consent-decline').addEventListener('click', function () {
555+
try { localStorage.setItem(key, 'declined'); } catch (e) {}
556+
hide();
557+
});
558+
})();
559+
</script>
560+
416561
<script>
417562
(function () {
418563
var LATEST = 'https://github.com/stuchain/CuePoint/releases/latest';
@@ -421,6 +566,19 @@ <h2>Download</h2>
421566
var statusEl = document.getElementById('dl-status');
422567
var versionEl = document.getElementById('dl-version');
423568

569+
function trackDownloadClick(el, fileExtension) {
570+
if (typeof gtag !== 'function') return;
571+
var href = el.getAttribute('href');
572+
if (!href || href === '#') return;
573+
gtag('event', 'file_download', {
574+
link_url: href,
575+
link_text: el.textContent.trim(),
576+
file_extension: fileExtension
577+
});
578+
}
579+
winEl.addEventListener('click', function () { trackDownloadClick(winEl, 'exe'); });
580+
macEl.addEventListener('click', function () { trackDownloadClick(macEl, 'dmg'); });
581+
424582
function finishLoading() {
425583
statusEl.classList.add('is-hidden');
426584
winEl.removeAttribute('aria-disabled');

gh-pages-root/robots.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
User-agent: *
2+
Allow: /
3+
4+
Sitemap: https://stuchain.github.io/CuePoint/sitemap.xml

gh-pages-root/sitemap.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3+
<url>
4+
<loc>https://stuchain.github.io/CuePoint/</loc>
5+
<changefreq>monthly</changefreq>
6+
<priority>1.0</priority>
7+
</url>
8+
</urlset>

scripts/publish_feeds.py

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
import subprocess
1313
import sys
1414
from pathlib import Path
15-
from typing import Optional
15+
from typing import Dict, Optional
1616

1717

1818
def run_command(cmd: list, cwd: Path = None) -> tuple:
@@ -89,6 +89,7 @@ def publish_feeds(
8989
print("Saving generated appcast content before branch checkout...")
9090
saved_index_content: Optional[str] = None
9191
saved_logo_bytes: Optional[bytes] = None
92+
saved_seo_static: Dict[str, str] = {}
9293
if index_path:
9394
index_file = Path(index_path)
9495
if not index_file.is_absolute():
@@ -100,6 +101,14 @@ def publish_feeds(
100101
if logo_file.exists():
101102
saved_logo_bytes = logo_file.read_bytes()
102103
print(f" Saved logo from: {logo_file.relative_to(repo_root)} ({len(saved_logo_bytes)} bytes)")
104+
for static_name in ("robots.txt", "sitemap.xml"):
105+
static_path = index_file.parent / static_name
106+
if static_path.exists():
107+
saved_seo_static[static_name] = static_path.read_text(encoding="utf-8")
108+
print(
109+
f" Saved {static_name} from: {static_path.relative_to(repo_root)} "
110+
f"({len(saved_seo_static[static_name])} bytes)"
111+
)
103112
else:
104113
print(f" Warning: Index file not found: {index_path}", file=sys.stderr)
105114
generated_appcast_content = {}
@@ -226,7 +235,13 @@ def publish_feeds(
226235
print(f" Restored logo: {logo_dest.relative_to(repo_root)}")
227236
run_command(["git", "add", "logo.png"], cwd=repo_root)
228237
print(" Added logo.png to git")
229-
238+
for static_name, static_content in saved_seo_static.items():
239+
static_dest = repo_root / static_name
240+
static_dest.write_text(static_content, encoding="utf-8")
241+
print(f" Restored {static_name}: {static_dest.relative_to(repo_root)}")
242+
run_command(["git", "add", static_name], cwd=repo_root)
243+
print(f" Added {static_name} to git")
244+
230245
# Copy appcast files to repository
231246
for appcast_file in appcast_files:
232247
appcast_path = Path(appcast_file)

0 commit comments

Comments
 (0)