diff --git a/README.md b/README.md index a3fbd9d..f755134 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,9 @@ played (notes, timing, held length and dynamics), and teaches with immediate, sp No account, no server, no tracking: **audio never leaves your device.** It installs as an app and works offline. +**Try it: [openkeys.live](https://openkeys.live)** · a free, open-source alternative to Yousician, Synthesia, +Simply Piano and flowkey for piano ([how it compares](https://openkeys.live/alternatives/)). + ![OpenKeys practice screen: sheet music with coloured notes, falling notes and the on-screen keyboard](docs/practice.png) ## What it does diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 317a396..6c0d4dc 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -124,3 +124,8 @@ frustrating feedback**. This file records those choices and why. 36. **Note names on the sheet are hand-coloured pills** (blue right hand, soft purple left hand, as on the keyboard and falling notes) in a row under each staff, so they never read as part of the black engraving (or its inverted dark version). Chords stack in pitch order, and the engraving reserves room for the row. +37. **Search pages are static HTML next to the app.** The app is a single client-rendered page, so the pages + that answer searches ("Yousician alternative", "Synthesia in the browser", "learn piano free") are plain + HTML written by `scripts/build-site.mjs` from `site/pages.mjs` after `vite build`, together with + `sitemap.xml`. They use the same tokens and fonts, need no JavaScript, and are excluded from the service + worker's app fallback. Claims about other apps stay general, dated and honest about what OpenKeys lacks. diff --git a/index.html b/index.html index 6f59618..7ecdc37 100644 --- a/index.html +++ b/index.html @@ -4,14 +4,80 @@ - + OpenKeys · Free piano teacher that listens to you play + + + + + + - OpenKeys + + + + + + + + + + + + + + + + +
- + diff --git a/package.json b/package.json index aee9bd1..7a8d74f 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "openkeys", "private": true, "version": "0.1.0", - "description": "Open-source, browser-based piano learning app: play, listen, judge, teach.", + "description": "Free, open-source piano teacher in the browser: listens through your mic or MIDI keyboard. A free alternative to Yousician, Synthesia, Simply Piano and flowkey.", "license": "MIT", "type": "module", "scripts": { @@ -10,7 +10,7 @@ "predev": "node scripts/copy-samples.mjs", "dev": "vite", "prebuild": "node scripts/copy-samples.mjs", - "build": "tsc -b && vite build", + "build": "tsc -b && vite build && node scripts/build-site.mjs", "preview": "vite preview", "typecheck": "tsc -b", "test": "vitest run", diff --git a/public/3071ecf16c40a239023848ab12f05eb7.txt b/public/3071ecf16c40a239023848ab12f05eb7.txt new file mode 100644 index 0000000..a84985b --- /dev/null +++ b/public/3071ecf16c40a239023848ab12f05eb7.txt @@ -0,0 +1 @@ +3071ecf16c40a239023848ab12f05eb7 \ No newline at end of file diff --git a/public/img/practice.jpg b/public/img/practice.jpg new file mode 100644 index 0000000..fcab8ab Binary files /dev/null and b/public/img/practice.jpg differ diff --git a/public/llms.txt b/public/llms.txt new file mode 100644 index 0000000..9e65998 --- /dev/null +++ b/public/llms.txt @@ -0,0 +1,29 @@ +# OpenKeys + +> OpenKeys (https://openkeys.live) is a free, open-source piano teacher that runs in the web browser. It listens to you play through the microphone (acoustic piano or keyboard) or a USB MIDI keyboard, waits on the right note, tells you what you played instead, and loops the bars you keep missing. No account, no subscription, no ads. Audio never leaves the device. MIT licensed. + +People often look for OpenKeys as a free or open-source alternative to Yousician, Synthesia (falling notes, in the browser), Simply Piano and flowkey, for piano only. + +## What it does + +- Input: microphone with a 2-minute setup wizard, USB MIDI keyboard (Chrome, Edge, Firefox; Safari is microphone only), or computer keys. +- Practice modes: Listen, Wait (stops until you play the right notes), Play along at 25-150% tempo, Follow me (follows your tempo), Loop drill (tempo rises 5% after each clean pass). Hands separately or together. +- Views: sheet music, falling notes, or both, with an on-screen keyboard. +- Feedback: notes marked right or wrong in place, wrong notes shown as ghost noteheads, results with a bar-by-bar heatmap, top trouble spots with a one-click practice loop, and coaching such as per-hand timing. +- Learning: a 7-lesson course, generated drills (scales and arpeggios in any key, chord progressions, sight-reading, rhythm, ear training, note reading), daily goal and streak, spaced review of trouble spots. +- Music: 15 built-in public-domain pieces in graded versions (Ode to Joy, Für Elise, Gymnopédie No. 1, Prelude in C, Minuet in G, Canon in D and more); import MusicXML/MXL, MIDI and ABC; built-in ABC editor. +- Free play with a live piano roll; record and transcribe to a score or MIDI, locally. +- Installs as an app and works offline. + +## Limits + +- Piano and keyboard only. +- A small built-in library (15 pieces) compared with commercial apps; no licensed pop songs; no video lessons. + +## Links + +- App: https://openkeys.live/ +- How it works: https://openkeys.live/learn-piano-free/ +- Comparison with Yousician, Synthesia, Simply Piano and flowkey: https://openkeys.live/alternatives/ +- Source code: https://github.com/PaulJoshi/OpenKeys +- Microphone accuracy measurements: https://github.com/PaulJoshi/OpenKeys/blob/main/docs/ACCURACY.md diff --git a/public/og-image.jpg b/public/og-image.jpg new file mode 100644 index 0000000..273de84 Binary files /dev/null and b/public/og-image.jpg differ diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..6601f1f --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://openkeys.live/sitemap.xml diff --git a/scripts/build-site.mjs b/scripts/build-site.mjs new file mode 100644 index 0000000..5b311b7 --- /dev/null +++ b/scripts/build-site.mjs @@ -0,0 +1,166 @@ +// Writes the static info pages (site/pages.mjs) into dist/ after `vite build`, plus sitemap.xml. +// The pages are plain HTML so search engines read them without running the app. They reuse the app's +// design tokens and the fonts Vite already emitted into dist/assets. +import { readFileSync, writeFileSync, mkdirSync, readdirSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { PAGES, SITE, COMPARED_ON, UPDATED, FEATURES } from '../site/pages.mjs'; + +const root = join(dirname(fileURLToPath(import.meta.url)), '..'); +const dist = join(root, 'dist'); +const base = process.env.BASE ?? '/'; + +const esc = (s) => s.replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); +const url = (slug) => `${SITE}/${slug ? slug + '/' : ''}`; +const href = (slug) => `${base}${slug ? slug + '/' : ''}`; + +// CSS: tokens with font URLs pointed at Vite's hashed copies, then the page styles. +const assets = readdirSync(join(dist, 'assets')); +const tokens = readFileSync(join(root, 'src/ui/design/tokens.css'), 'utf8').replace(/url\("\.\/fonts\/([^".]+)\.woff2"\)/g, (m, name) => { + const file = assets.find((f) => f.startsWith(name + '-') && f.endsWith('.woff2')); + if (!file) throw new Error(`build-site: no emitted font for ${name}; run vite build first`); + return `url("${base}assets/${file}")`; +}); +writeFileSync(join(dist, 'site.css'), tokens + '\n' + readFileSync(join(root, 'site/site.css'), 'utf8')); + +const CHEVRON = + ''; +const stripTags = (s) => s.replace(/<[^>]+>/g, ''); + +const header = (page) => `
+OpenKeys +
`; + +const footer = () => ``; + +const table = (t) => `
+${t.cols.map((c) => ``).join('')} +${t.rows.map(([label, ...cells]) => `${cells.map((c) => ``).join('')}`).join('\n')} +
${esc(c)}
${esc(label)}${esc(c)}
+

Compared in ${COMPARED_ON} from each app's public information. Prices and features change, so check their sites.

`; + +const faq = (items) => `
${items + .map(([q, a]) => `
${esc(q)}${CHEVRON}

${esc(a)}

`) + .join('\n')}
`; + +const tiles = () => `
${FEATURES.map(([h, p]) => `

${esc(h)}

${esc(p)}

`).join('')}
`; + +const related = (page) => { + const others = PAGES.filter((p) => p.slug !== page.slug); + const sub = (p) => p.title.split(' · ')[0]; + return ``; +}; + +function jsonLd(page) { + const crumbs = [{ name: 'OpenKeys', item: url('') }]; + if (page.slug !== 'alternatives' && page.them) crumbs.push({ name: 'Compare', item: url('alternatives') }); + crumbs.push({ name: page.nav, item: url(page.slug) }); + return [ + { + '@context': 'https://schema.org', + '@type': 'WebPage', + name: page.title, + description: page.description, + url: url(page.slug), + dateModified: UPDATED, + inLanguage: 'en', + isPartOf: { '@type': 'WebSite', name: 'OpenKeys', url: url('') }, + about: { '@type': 'WebApplication', name: 'OpenKeys', url: url(''), applicationCategory: 'EducationalApplication', operatingSystem: 'Any', offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' } }, + }, + { + '@context': 'https://schema.org', + '@type': 'BreadcrumbList', + itemListElement: crumbs.map((c, i) => ({ '@type': 'ListItem', position: i + 1, name: c.name, item: c.item })), + }, + { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: page.faq.map(([q, a]) => ({ '@type': 'Question', name: q, acceptedAnswer: { '@type': 'Answer', text: stripTags(a) } })), + }, + ]; +} + +function render(page) { + const them = page.them; + return ` + + + + +${esc(page.title)} + + + + + + + + + + + + + + + + + + + + +${jsonLd(page).map((d) => ``).join('\n')} + + +${header(page)} +
+

OpenKeys${them ? ` / Compare` : ''} / ${esc(page.nav)}

+
+

${esc(page.h1)}

+

${esc(page.lede)}

+ +
OpenKeys practice screen: sheet music with notes marked right and wrong, falling notes and an on-screen piano keyboard
Practising in OpenKeys: sheet music, falling notes and the keyboard, in a browser.
+
+
+${page.intro.map((p) => `

${esc(p)}

`).join('\n')} +
+${page.table ? `

${them ? `OpenKeys and ${esc(them.name)} side by side` : 'At a glance'}

${table(page.table)}
` : ''} +${them ? `

When OpenKeys is the better fit

    ${page.better.map((b) => `
  • ${esc(b)}
  • `).join('')}
+

When ${esc(them.name)} is the better fit

    ${page.worse.map((b) => `
  • ${esc(b)}
  • `).join('')}
` : ''} +${(page.sections ?? []).map((s) => `

${esc(s.h2)}

${s.html}
`).join('\n')} +

What OpenKeys does

${tiles()}
+

Questions

${faq(page.faq)}
+

Try it now

OpenKeys opens in your browser in a few seconds. No account, nothing to install.

+

More

${related(page)}
+
+${footer()} + + +`; +} + +for (const page of PAGES) { + const dir = join(dist, page.slug); + mkdirSync(dir, { recursive: true }); + writeFileSync(join(dir, 'index.html'), render(page)); +} + +const sitemap = ` + +${['', ...PAGES.map((p) => p.slug)].map((s) => ` ${url(s)}${UPDATED}`).join('\n')} + +`; +writeFileSync(join(dist, 'sitemap.xml'), sitemap); +console.log(`build-site: ${PAGES.length} pages and sitemap.xml written to dist/`); diff --git a/site/pages.mjs b/site/pages.mjs new file mode 100644 index 0000000..1a50795 --- /dev/null +++ b/site/pages.mjs @@ -0,0 +1,303 @@ +// Content for the static info pages served next to the app (built by scripts/build-site.mjs). +// Each page answers one search intent ("Yousician alternative", "Synthesia in the browser", ...) with real, +// specific content. Copy rules are the design system's: plain, short, sentence case, numerals, no hype. +// Claims about other apps stay general and dated; check them again before changing COMPARED_ON. + +export const SITE = 'https://openkeys.live'; +export const COMPARED_ON = 'November 2026'; +export const UPDATED = '2026-11-03'; + +// What OpenKeys is, in one place, so every page says the same thing. +export const FEATURES = [ + ['Listens through your microphone', 'Put a laptop or tablet on the music stand of any piano or keyboard. No cable needed. A setup wizard tunes it to your room in about 2 minutes.'], + ['Exact with a MIDI keyboard', 'Plug a USB MIDI keyboard into Chrome, Edge or Firefox and every note, its timing and how hard you played it are read exactly.'], + ['Waits for the right note', 'Wait mode stops until you play the right keys, then tells you what you played instead: "That was E4; try D4, 2 keys lower".'], + ['Falling notes and sheet music', 'Practise with sheet music, falling notes or both. Notes turn green or red in place, and wrong notes show as ghost noteheads.'], + ['Loops the bars you miss', 'Results show your top trouble spots bar by bar. One click loops them, and Loop drill raises the tempo 5% after each clean pass.'], + ['Your own music', 'Import MusicXML (from MuseScore), MIDI and ABC files by drag and drop, or write exercises in the built-in editor.'], + ['A course and drills', 'A 7-lesson course from finding middle C to playing with both hands, plus scales, arpeggios, chords, sight-reading, rhythm and ear training.'], + ['Free, with no account', 'No sign-up, no subscription, no ads and no trial. MIT licensed and open source on GitHub.'], + ['Private and offline', 'Audio never leaves your device. Your progress is stored in the browser, and OpenKeys installs as an app that works offline.'], +]; + +const OPENKEYS_COLUMN = { + price: 'Free. No subscription, no ads, no in-app purchases', + account: 'Not needed', + where: 'Any modern browser on a computer, tablet or phone; installs as an app', + mic: 'Yes, with a setup wizard for your room', + midi: 'Yes, in Chrome, Edge and Firefox (Safari: microphone only)', + files: 'Yes: MusicXML, MIDI and ABC', + library: '15 built-in public-domain pieces in graded versions, plus anything you import', + instruments: 'Piano and keyboard', + source: 'Open source, MIT licence', + privacy: 'Audio and progress stay on your device', +}; + +const ROWS = [ + ['price', 'Price'], + ['account', 'Account'], + ['where', 'Where it runs'], + ['mic', 'Listens through a microphone'], + ['midi', 'MIDI keyboard'], + ['files', 'Import your own files'], + ['library', 'Songs'], + ['instruments', 'Instruments'], + ['source', 'Source code'], + ['privacy', 'Your data'], +]; + +const SHARED_FAQ = [ + ['Is OpenKeys really free?', 'Yes. There is no subscription, no trial, no ads and nothing to unlock. The code is MIT licensed on GitHub, so it stays available even if the project stops.'], + ['Do I need to download anything or make an account?', 'No. Open openkeys.live in Chrome, Edge, Firefox or Safari and start playing. You can install it as an app from the browser if you want it offline.'], + ['Does it work with an acoustic piano?', 'Yes. OpenKeys listens through your device microphone. Single notes are detected precisely; for chords it checks the notes it expects. When it is not sure, it shows "?" and never counts that against you.'], + ['What do I need to start?', 'A piano or keyboard and a computer, tablet or phone. A USB MIDI cable is the most accurate. With no instrument at all, you can play with the computer keys.'], +]; + +export const PAGES = [ + { + slug: 'learn-piano-free', + nav: 'Learn free', + title: 'Learn piano online for free, in your browser · OpenKeys', + description: + 'OpenKeys is a free piano teacher that runs in your browser. It listens through your microphone or MIDI keyboard, waits on the right note and loops the bars you miss. No account, no download.', + h1: 'Learn piano online for free, in your browser', + lede: 'OpenKeys listens while you play and tells you what went well and what to fix. No account, no subscription, nothing to install.', + intro: [ + 'Most piano apps are free to download and then ask for a subscription. OpenKeys is free all the way through: every lesson, drill and practice mode, with no limits on how long you play.', + 'It works with the instrument you already have. Play an acoustic piano or a keyboard into the microphone, connect a MIDI keyboard over USB, or use the computer keys if you have no instrument yet.', + ], + sections: [ + { + h2: 'How it works', + html: `
+

1. Choose how you play

Microphone, MIDI keyboard or computer keys. The setup wizard checks room noise, timing and tuning.

+

2. Pick a lesson or a piece

Start the 7-lesson course, or open Ode to Joy, Für Elise, Minuet in G and more in graded versions.

+

3. Play and get feedback

OpenKeys plays the piece first, then follows you, marks each note and shows exactly what to practise next.

+
`, + }, + { + h2: 'Practice modes', + html: ` +

Practise hands separately or together. OpenKeys can play the other hand for you.

`, + }, + ], + faq: [ + ['Can I learn piano with an app alone?', 'An app can teach you to read music, find notes, keep time and practise efficiently, and it is a good start. A teacher is still the best way to fix posture and technique, so OpenKeys works well between lessons too.'], + ['Which songs are included?', 'Twinkle Twinkle, Ode to Joy, Für Elise, Gymnopédie No. 1, Bach\'s Prelude in C, Minuet in G, Canon in D, Greensleeves, Amazing Grace and more, each in easy and fuller versions. You can import any MusicXML, MIDI or ABC file.'], + ...SHARED_FAQ, + ], + }, + { + slug: 'alternatives', + nav: 'Compare', + title: 'Free piano learning apps compared: Yousician, Synthesia, Simply Piano, flowkey · OpenKeys', + description: + 'How OpenKeys, a free and open-source piano teacher in the browser, compares with Yousician, Synthesia, Simply Piano and flowkey: price, account, microphone and MIDI, your own files.', + h1: 'Piano learning apps compared', + lede: 'OpenKeys is a free, open-source piano teacher that runs in your browser. Here is how it compares with the apps you may already know.', + intro: [ + 'Yousician, Simply Piano and flowkey are polished apps with large licensed song libraries, paid by subscription. Synthesia is a falling-notes app with a one-time unlock. OpenKeys is free and open source, runs in any browser, needs no account and keeps your audio on your device.', + 'OpenKeys has fewer built-in songs than any of them. It makes up for that by importing the MusicXML, MIDI and ABC files you already have.', + ], + table: { + cols: ['OpenKeys', 'Yousician', 'Synthesia', 'Simply Piano', 'flowkey'], + rows: [ + ['Price', 'Free', 'Subscription, limited free tier', 'Free with a few songs, one-time unlock', 'Subscription, limited free tier', 'Subscription, a few free songs'], + ['Account', 'Not needed', 'Needed', 'Not needed', 'Needed', 'Needed'], + ['Runs in a web browser', 'Yes', 'Mainly apps', 'No, installed app', 'No, phone and tablet app', 'Yes, and apps'], + ['Listens through a microphone', 'Yes', 'Yes', 'No, MIDI keyboard', 'Yes', 'Yes'], + ['MIDI keyboard', 'Yes', 'Yes', 'Yes', 'Yes', 'Yes'], + ['Import your own MIDI or MusicXML', 'Yes', 'No', 'MIDI, after unlock', 'No', 'No'], + ['Open source', 'Yes, MIT', 'No', 'No', 'No', 'No'], + ], + }, + sections: [], + faq: [ + ['What is the best free alternative to Yousician or Simply Piano?', 'If you want to learn piano without a subscription, OpenKeys is free with no limits, runs in the browser and listens through your microphone or a MIDI keyboard. It has fewer built-in songs, but you can import your own.'], + ['Is there a web version of Synthesia?', 'Synthesia itself is an installed app. OpenKeys gives you falling notes, sheet music and your own MIDI files in the browser, for free. See the Synthesia comparison below.'], + ...SHARED_FAQ, + ], + }, + { + slug: 'yousician-alternative', + nav: 'Yousician', + title: 'Free Yousician alternative for piano, open source · OpenKeys', + description: + 'Looking for a free Yousician alternative? OpenKeys is an open-source piano teacher in your browser. It listens through your mic or MIDI keyboard, with no account, no subscription and no daily limit.', + h1: 'A free, open-source Yousician alternative for piano', + lede: 'OpenKeys listens to you play, like Yousician, but it is free with no daily limit, runs in your browser and needs no account.', + intro: [ + 'Yousician teaches guitar, piano, bass, ukulele and singing, with a large library of licensed songs and a subscription for full access. If you only want piano and do not want to pay monthly, OpenKeys covers the core of it: it listens through your microphone or MIDI keyboard, marks every note and tells you what to practise next.', + 'OpenKeys is open source. There is no account, no paywall and no limit on how long you practise each day. Your audio never leaves your device.', + ], + them: { + name: 'Yousician', + price: 'Subscription for full access; a limited free tier', + account: 'Needed', + where: 'Apps for phone, tablet and computer', + mic: 'Yes', + midi: 'Yes', + files: 'No, its own catalogue', + library: 'Large licensed catalogue, including pop songs', + instruments: 'Guitar, piano, bass, ukulele and singing', + source: 'Closed source', + privacy: 'Account and progress stored by the service', + }, + better: [ + 'You want to learn piano without a subscription or a daily time limit.', + 'You want to play your own pieces: import MusicXML from MuseScore, or MIDI and ABC files.', + 'You prefer to practise in the browser on any computer, with no account and no tracking.', + 'You want specific feedback: wrong notes shown as ghost noteheads, timing per hand and a bar-by-bar heatmap.', + ], + worse: [ + 'Yousician has far more songs, including licensed pop music. OpenKeys has 15 public-domain pieces plus your imports.', + 'Yousician teaches guitar, bass, ukulele and singing too. OpenKeys is piano only.', + 'Yousician has video lessons and a bigger structured curriculum. OpenKeys has a 7-lesson course and generated drills.', + ], + faq: [ + ['Is there a free version of Yousician?', 'Yousician has a free tier with limits; full access is a subscription. OpenKeys is a different app that is free with no limits for piano.'], + ['Is there an open-source Yousician?', 'OpenKeys is an open-source piano teacher in the same spirit: it listens to you play and gives feedback. The code is on GitHub under the MIT licence.'], + ['Does OpenKeys work like Yousician with a microphone?', 'Yes. Put your device near the piano, run the 2-minute setup wizard and play. OpenKeys follows you note by note.'], + ...SHARED_FAQ, + ], + }, + { + slug: 'synthesia-alternative', + nav: 'Synthesia', + title: 'Synthesia alternative in your browser, free and online · OpenKeys', + description: + 'Looking for Synthesia online or a web version? OpenKeys runs in the browser with falling notes, sheet music, your own MIDI files, MIDI keyboard and microphone input. Free and open source.', + h1: 'Falling notes in your browser. A free Synthesia alternative.', + lede: 'Open a MIDI file, watch the notes fall and play along on your keyboard. In the browser, free, with no download.', + intro: [ + 'Synthesia made falling-notes piano popular: load a MIDI file, watch the notes come down and play them on a MIDI keyboard. It is an installed app with a free set of songs and a one-time unlock for the rest.', + 'OpenKeys does the same in a web browser, for free. It also shows the sheet music, listens through a microphone if you have no MIDI keyboard, and gives feedback on every note: right, wrong, early, late or held too short.', + ], + them: { + name: 'Synthesia', + price: 'Free with a set of songs; one-time unlock for the rest and your own MIDI files', + account: 'Not needed', + where: 'Installed app for computer, tablet and phone', + mic: 'No, made for MIDI keyboards', + midi: 'Yes', + files: 'MIDI, after unlock', + library: 'Built-in songs plus any MIDI file', + instruments: 'Piano and keyboard', + source: 'Closed source', + privacy: 'Runs on your device', + }, + better: [ + 'You want falling notes without installing anything, on any computer with a browser.', + 'You have an acoustic piano or a keyboard without USB: OpenKeys listens through the microphone.', + 'You want to learn to read music: sheet music and falling notes can show side by side.', + 'You want to import MIDI and MusicXML files for free, and record your playing back to MIDI.', + ], + worse: [ + 'Synthesia\'s falling-notes view is its whole focus and is very polished; OpenKeys splits its attention between sheet music and falling notes.', + 'Synthesia has a large built-in song set. OpenKeys has 15 pieces, plus any file you import.', + 'Safari cannot use MIDI keyboards (no Web MIDI), so on a Mac or iPad with Safari OpenKeys uses the microphone. Chrome, Edge and Firefox support MIDI.', + ], + faq: [ + ['Is there a web version of Synthesia?', 'Synthesia is an installed app. If you want falling notes in the browser, OpenKeys does it for free: open openkeys.live, import a MIDI file and play.'], + ['Can I play Synthesia-style MIDI files in OpenKeys?', 'Yes. Drag a .mid file onto the Library page. OpenKeys splits the hands, shows sheet music and falling notes, and judges your playing.'], + ['Do I need a MIDI keyboard?', 'No. A MIDI keyboard over USB is the most accurate, but OpenKeys also listens through a microphone, and you can play with the computer keys.'], + ...SHARED_FAQ, + ], + }, + { + slug: 'simply-piano-alternative', + nav: 'Simply Piano', + title: 'Free Simply Piano alternative with no subscription · OpenKeys', + description: + 'A free alternative to Simply Piano: OpenKeys is an open-source piano teacher in your browser that listens through your mic or MIDI keyboard. No subscription, no account, works on a laptop.', + h1: 'A free Simply Piano alternative, with no subscription', + lede: 'OpenKeys listens to your piano and guides you note by note, like Simply Piano, but it is free and runs in a browser on any computer.', + intro: [ + 'Simply Piano is a well-made app for phones and tablets that listens to you play and walks you through songs, with a subscription for most of its content. OpenKeys does the listening and the guidance for free, in the browser, on a laptop, tablet or phone.', + 'There is no sign-up and no trial to cancel. Everything, from the course to the drills, is open from the first minute.', + ], + them: { + name: 'Simply Piano', + price: 'Subscription for full access; a few songs free', + account: 'Needed', + where: 'Phone and tablet app', + mic: 'Yes', + midi: 'Yes', + files: 'No, its own catalogue', + library: 'Large licensed catalogue, including pop songs', + instruments: 'Piano', + source: 'Closed source', + privacy: 'Account and progress stored by the service', + }, + better: [ + 'You want to learn piano without a subscription or a trial.', + 'You want to practise on a laptop or desktop browser, with a bigger view of the music.', + 'You want to import your own pieces as MusicXML, MIDI or ABC.', + 'You want detailed feedback: per-hand timing, a bar-by-bar heatmap and loops of the bars you miss.', + ], + worse: [ + 'Simply Piano has many more songs, including licensed pop music. OpenKeys has 15 public-domain pieces plus your imports.', + 'Simply Piano has a longer guided course. OpenKeys has a 7-lesson course plus generated drills.', + ], + faq: [ + ['Is there a free Simply Piano alternative?', 'Yes. OpenKeys is free with no subscription. It listens through your microphone or MIDI keyboard and runs in the browser.'], + ['Does OpenKeys work on a laptop?', 'Yes. OpenKeys runs in Chrome, Edge, Firefox and Safari on Windows, macOS, Linux and ChromeOS, and on tablets and phones.'], + ...SHARED_FAQ, + ], + }, + { + slug: 'flowkey-alternative', + nav: 'flowkey', + title: 'Free flowkey alternative, open source, in your browser · OpenKeys', + description: + 'A free alternative to flowkey: OpenKeys is an open-source piano teacher in your browser with sheet music, Wait mode, microphone and MIDI input. No account and no subscription.', + h1: 'A free, open-source flowkey alternative', + lede: 'Sheet music, a mode that waits for the right note, and feedback through your microphone or MIDI keyboard. Free, in your browser.', + intro: [ + 'flowkey teaches with sheet music next to video of a real pianist\'s hands, and waits for you to play the right notes. Full access is a subscription. OpenKeys also runs in the browser and waits for the right notes, and it is free and open source.', + 'OpenKeys goes further on feedback: notes are marked in place on the sheet music, wrong notes appear as ghost noteheads, and your trouble spots become short loops you can drill.', + ], + them: { + name: 'flowkey', + price: 'Subscription for full access; a few songs free', + account: 'Needed', + where: 'Web browser and apps', + mic: 'Yes', + midi: 'Yes', + files: 'No, its own catalogue', + library: 'Large catalogue, including pop and film music', + instruments: 'Piano', + source: 'Closed source', + privacy: 'Account and progress stored by the service', + }, + better: [ + 'You want Wait mode and sheet-music practice without a subscription or account.', + 'You want to import your own scores from MuseScore (MusicXML), MIDI or ABC.', + 'You want detailed feedback after each take, and loops of the bars you miss.', + 'You want your audio and progress to stay on your device.', + ], + worse: [ + 'flowkey has video of a pianist\'s hands for each song. OpenKeys shows notes on the sheet, falling notes and an on-screen keyboard, but no video.', + 'flowkey has a much larger song catalogue. OpenKeys has 15 public-domain pieces plus your imports.', + ], + faq: [ + ['Is there a free flowkey alternative?', 'Yes. OpenKeys is free with no subscription, runs in the browser and waits for the right notes like flowkey does.'], + ...SHARED_FAQ, + ], + }, +]; + +// Fill each competitor page's two-column table from the shared rows. +for (const p of PAGES) { + if (!p.them) continue; + p.table = { + cols: ['OpenKeys', p.them.name], + rows: ROWS.map(([k, label]) => [label, OPENKEYS_COLUMN[k], p.them[k]]), + }; +} diff --git a/site/site.css b/site/site.css new file mode 100644 index 0000000..278ebe3 --- /dev/null +++ b/site/site.css @@ -0,0 +1,140 @@ +/* + * Styles for the static info pages (site/pages.mjs, built by scripts/build-site.mjs). Same design system as + * the app: ink / white / soft-cloud, Inter 400/500, Bebas Neue for the hero line only, pill controls, square + * flat blocks, no shadows, no hover tints, "tap collapse" press. Tokens are prepended at build time from + * src/ui/design/tokens.css, so every colour here is a variable. + */ +@media (prefers-color-scheme: dark) { + :root:not([data-theme='light']) { + --surface-canvas: var(--ok-ink); + --surface-soft: var(--ok-charcoal); + --color-primary: var(--ok-white); + --color-on-primary: var(--ok-ink); + --text-primary: var(--ok-white); + --text-soft: var(--ok-hairline-soft); + --text-secondary: var(--ok-stone); + --border-hairline: var(--ok-ash); + --border-hairline-soft: var(--ok-ash); + --elevation-inset: inset 0 -1px 0 var(--ok-charcoal); + color-scheme: dark; + } +} + +* { box-sizing: border-box; } +html { -webkit-text-size-adjust: 100%; } +body { + margin: 0; + background: var(--surface-canvas); + color: var(--text-primary); + font-family: var(--font-sans); + font-weight: var(--weight-regular); + font-size: var(--type-body-size); + line-height: var(--type-body-lh); +} +b, strong, th { font-weight: var(--weight-medium); } +a { color: inherit; } +img { display: block; max-width: 100%; height: auto; } + +.wrap { max-width: 1120px; margin: 0 auto; padding: 0 var(--gutter-desktop); } + +/* Header */ +.top { + position: sticky; top: 0; z-index: 1; + background: var(--surface-canvas); + box-shadow: var(--elevation-inset); +} +.top .wrap { height: var(--nav-primary-h); display: flex; align-items: center; gap: var(--space-xl); } +.brand { font-weight: var(--weight-medium); font-size: 20px; line-height: 1; text-decoration: none; } +.top nav { display: flex; gap: var(--space-xl); margin-left: auto; align-items: center; } +.top nav a { text-decoration: none; font-weight: var(--weight-medium); white-space: nowrap; } +.top nav a[aria-current='page'] { box-shadow: inset 0 -2px 0 var(--text-primary); } + +/* Buttons */ +.btn { + display: inline-flex; align-items: center; justify-content: center; gap: var(--space-sm); + min-height: var(--control-h); padding: 0 32px; + border-radius: var(--radius-button); + background: var(--surface-soft); color: var(--text-primary); + font-weight: var(--weight-medium); text-decoration: none; white-space: nowrap; + transition: transform var(--duration-base) var(--ease-standard), opacity var(--duration-base) var(--ease-standard); +} +.btn.sm { min-height: var(--control-h-sm); padding: 0 18px; font-size: var(--type-button-sm-size); } +.btn.primary { background: var(--color-primary); color: var(--color-on-primary); } +.btn:active { transform: scale(var(--press-scale)); opacity: var(--press-opacity); } +.row { display: flex; flex-wrap: wrap; gap: var(--space-sm); align-items: center; } + +/* Sections */ +main section { padding: var(--space-section) 0; } +main section + section { border-top: 1px solid var(--border-hairline-soft); } +.crumbs { margin: var(--space-xl) 0 0; color: var(--text-secondary); font-size: var(--type-caption-md-size); } +.crumbs a { text-decoration: none; } +.display { + margin: 0 0 var(--space-xl); + font-family: var(--font-display); font-weight: var(--weight-regular); + font-size: var(--type-display-size); line-height: var(--type-display-lh); + text-transform: uppercase; max-width: 20ch; +} +.lede { font-size: var(--type-heading-lg-size); line-height: 1.35; color: var(--text-soft); max-width: 40ch; margin: 0 0 var(--space-xxl); } +h2 { font-weight: var(--weight-medium); font-size: var(--type-heading-xl-size); line-height: var(--type-heading-xl-lh); margin: 0 0 var(--space-xl); } +h3 { font-weight: var(--weight-medium); font-size: var(--type-heading-md-size); line-height: var(--type-heading-md-lh); margin: 0 0 var(--space-xs); } +p { margin: 0 0 var(--space-md); max-width: 68ch; } +.note { color: var(--text-secondary); font-size: var(--type-caption-md-size); } +ul.plain { margin: 0; padding-left: 20px; max-width: 68ch; } +ul.plain li { margin-bottom: var(--space-sm); } + +.shot { margin: var(--space-section) 0 0; background: var(--surface-soft); } +.shot figcaption { padding: var(--space-md) 0 0; color: var(--text-secondary); font-size: var(--type-caption-md-size); background: var(--surface-canvas); } + +/* Feature tiles: square, flat, soft-cloud */ +.tiles { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-sm); } +.tile { background: var(--surface-soft); padding: var(--space-xl); } +.tile p { margin: 0; color: var(--text-soft); } + +/* Comparison table */ +.table-scroll { overflow-x: auto; -webkit-overflow-scrolling: touch; } +table { border-collapse: collapse; width: 100%; min-width: 560px; } +th, td { text-align: left; vertical-align: top; padding: var(--space-md) var(--space-lg) var(--space-md) 0; border-bottom: 1px solid var(--border-hairline); } +thead th { font-size: var(--type-caption-md-size); color: var(--text-secondary); } +tbody th { width: 26%; } + +/* FAQ: disclosure rows */ +.faq { border-top: 1px solid var(--border-hairline); max-width: 820px; } +.faq details { border-bottom: 1px solid var(--border-hairline); } +.faq summary { + list-style: none; cursor: pointer; display: flex; justify-content: space-between; align-items: center; gap: 16px; + padding: var(--space-xl) 0; font-weight: var(--weight-medium); line-height: var(--type-heading-md-lh); +} +.faq summary::-webkit-details-marker { display: none; } +.faq summary svg { flex: none; transition: transform var(--duration-base) var(--ease-standard); } +.faq details[open] summary svg { transform: rotate(180deg); } +.faq .a { padding-bottom: var(--space-xl); } + +/* Link list (related pages) */ +.links { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: var(--space-sm); } +.links a { display: block; background: var(--surface-soft); padding: var(--space-xl); text-decoration: none; } +.links a span { display: block; color: var(--text-secondary); font-size: var(--type-caption-md-size); } + +/* Footer */ +footer { border-top: 1px solid var(--border-hairline); padding: var(--space-section) 0 var(--space-xl); color: var(--text-secondary); } +footer .cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-xl); } +footer .cols div { display: flex; flex-direction: column; gap: var(--space-md); } +footer .cols b { color: var(--text-primary); } +footer .cols a { text-decoration: none; font-weight: var(--weight-medium); font-size: var(--type-caption-md-size); } +footer .legal { border-top: 1px solid var(--border-hairline); margin-top: var(--space-section); padding-top: var(--space-md); font-size: var(--type-caption-sm-size); } + +@media (max-width: 1023px) { + .display { font-size: var(--type-display-size-tablet); } + .tiles { grid-template-columns: repeat(2, minmax(0, 1fr)); } + main section { padding: var(--space-section-tablet) 0; } +} +@media (max-width: 599px) { + .wrap { padding: 0 16px; } + .display { font-size: var(--type-display-size-mobile); } + .lede { font-size: 20px; } + h2 { font-size: var(--type-heading-lg-size); } + .tiles, .links, footer .cols { grid-template-columns: 1fr; } + main section { padding: var(--space-section-mobile) 0; } + .top nav .hide-sm { display: none; } + .top .wrap { gap: var(--space-md); } + .top nav { gap: var(--space-md); } +} diff --git a/src/ui/screens/Today.tsx b/src/ui/screens/Today.tsx index 260340b..bacf0d2 100644 --- a/src/ui/screens/Today.tsx +++ b/src/ui/screens/Today.tsx @@ -90,6 +90,10 @@ export function Today() { No instrument +

+ How OpenKeys works ·{' '} + How it compares with other piano apps +

)}
diff --git a/vercel.json b/vercel.json index a29df42..45f7868 100644 --- a/vercel.json +++ b/vercel.json @@ -4,6 +4,14 @@ "installCommand": "npm ci", "buildCommand": "npm run build", "outputDirectory": "dist", + "redirects": [ + { + "source": "/:path*", + "has": [{ "type": "host", "value": "openkeys-learn.vercel.app" }], + "destination": "https://openkeys.live/:path*", + "permanent": true + } + ], "headers": [ { "source": "/sw.js", diff --git a/vite.config.ts b/vite.config.ts index ec3ed08..152e8e4 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -16,7 +16,8 @@ export default defineConfig({ manifest: { name: 'OpenKeys — piano teacher', short_name: 'OpenKeys', - description: 'Open-source piano learning in the browser: play, listen, judge, teach.', + description: 'A free, open-source piano teacher in your browser. It listens through your microphone or MIDI keyboard.', + categories: ['education', 'music'], theme_color: '#111111', background_color: '#ffffff', display: 'standalone', @@ -35,6 +36,8 @@ export default defineConfig({ globIgnores: ['samples/**', 'models/**'], maximumFileSizeToCacheInBytes: 8 * 1024 * 1024, navigateFallback: 'index.html', + // The static info pages (site/pages.mjs) are written after this build; never answer them with the app. + navigateFallbackDenylist: [/^\/(learn-piano-free|alternatives|[a-z-]+-alternative)\/?/], runtimeCaching: [ { urlPattern: ({ url }) => url.pathname.includes('/models/'),