v2: light editorial redesign, bilingual (EN-first), preview, front matter, PWA

Design (design-taste-frontend + ui-ux-design-pro + impeccable):
- Warm, flat, light editorial theme (OKLCH), Geist + Geist Mono, single vermilion accent
- Dark mode toggle, persisted; respects prefers-color-scheme
- Prominent focus rings, tactile states, reduced-motion

Features (GitHub-ready):
- i18n EN/DE, English default, persisted
- Live split preview (marked + DOMPurify, sanitized)
- Optional YAML front matter (title/date/source/tags), rendered as metadata block
- Conversion options: links as text, strip images, bullet marker, heading style
- Filename patterns (title+date / date+time), auto/custom indicator
- Drag & drop .html/.md/.txt, reading time
- Installable PWA + service worker (offline), README (EN) + MIT LICENSE

Fix: switch inputs were covered by decorative spans (pointer-events).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 11:25:42 +00:00
parent f2793a34c3
commit 197376a584
13 changed files with 787 additions and 572 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ FROM nginx:1.27-alpine
# Static single-page app — no build step.
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY index.html /usr/share/nginx/html/index.html
COPY index.html manifest.webmanifest sw.js icon.svg /usr/share/nginx/html/
COPY vendor/ /usr/share/nginx/html/vendor/
EXPOSE 80
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Till Heidrich (Heidrich Digital)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+64 -29
View File
@@ -1,51 +1,86 @@
# clip2md — Paste to Markdown
Winziges, self-hosted Tool: langen Text (Reddit-Thread, Website, formatierter Content) reinpasten
→ ein Klick / `⌘/Strg+S` → als `.md` speichern. Dateiname wird automatisch aus Titel + Datum vorgeschlagen.
Paste a long Reddit thread, an article, or any formatted web content, and save it as clean
Markdown in a single keystroke. Runs entirely in your browser: nothing is uploaded, no account,
no tracking, works offline.
**Live:** https://clip2md.heidrich-digital.de
**Live demo:** https://clip2md.heidrich-digital.de
## Was es macht
![clip2md](docs/screenshot-light.png)
- **Smart Paste:** Enthält die Zwischenablage formatiertes HTML (Copy aus dem Browser, z.B. Reddit
`⌘A``⌘C`), wird es automatisch über [Turndown](https://github.com/mixmark-io/turndown)
(+ GFM-Plugin für Tabellen/Strikethrough/Task-Lists) zu sauberem Markdown konvertiert.
Reiner Text ohne HTML bleibt 1:1 erhalten.
- **Roh erzwingen:** `⌘/Strg+⇧+V` fügt beim nächsten Paste bewusst als Klartext ein (keine Konvertierung).
- **Auto-Dateiname:** aus der ersten Überschrift/Zeile + Datum, z.B. `2026-08-11-mein-thread.md`.
Editierbar — sobald du selbst tippst, wird nicht mehr überschrieben.
- **Speichern:** Button oder `⌘/Strg+S` lädt die `.md` herunter (Browser-Download, kein Server-Roundtrip).
- **Kopieren / Leeren**, Wort- und Zeichenzähler.
## Why
Alles läuft **komplett clientseitig** — nichts wird an einen Server geschickt.
Copying from the web and getting usable Markdown is usually two or three tools and a lot of
cleanup. clip2md collapses that into: paste, press save. Rich HTML from the clipboard is
converted to tidy Markdown automatically (via [Turndown](https://github.com/mixmark-io/turndown)
with GitHub-flavored tables, strikethrough and task lists); plain text is kept verbatim. The
filename is proposed for you from the first heading plus the date, so it drops straight into an
Obsidian vault, a wiki, or a repo.
## Features
- **Smart paste.** Formatted HTML (copy from a browser, e.g. Reddit `⌘A``⌘C`) is converted to
clean Markdown. Plain text stays raw. Same `⌘V` for both, no mode switch.
- **Force raw.** `⌘/Ctrl+Shift+V` pastes the next clip as plain text, skipping conversion.
- **Live preview.** Toggle a side-by-side rendered Markdown view (marked + DOMPurify).
- **Auto filename.** Derived from the first heading + date, e.g. `2026-08-11-my-thread.md`.
Editable — once you type your own, it stops overwriting. Switch to a date+time pattern in settings.
- **YAML front matter.** Optional: prepend `title`, `date`, `source` URL and `tags` for
knowledge bases like Obsidian.
- **Conversion options.** Links as plain text, strip images, bullet marker (`-` / `*` / `+`),
heading style (ATX `#` / Setext).
- **Drag & drop.** Drop an `.html`, `.md` or `.txt` file to load or convert it.
- **Bilingual.** English and German, switchable in the header.
- **Light & dark.** Warm, flat, editorial light theme by default; dark mode one click away.
- **Save / copy / clear**, word + character count and reading time.
- **Installable PWA**, fully offline after first load.
Everything is **client-side**. No servers, no CDNs, no third-party requests — GDPR-friendly by
construction.
## Keyboard shortcuts
| Action | Shortcut |
| --- | --- |
| Save `.md` | `⌘/Ctrl + S` |
| Paste (smart) | `⌘/Ctrl + V` |
| Paste raw | `⌘/Ctrl + Shift + V` |
| Close settings | `Esc` |
## Tech
- Statische `index.html` (inline CSS/JS, OKLCH-Design, Dark).
- Vendored: `vendor/turndown.js`, `vendor/turndown-plugin-gfm.js` (kein CDN, offline-fähig).
- Ausgeliefert via `nginx:alpine` (siehe `Dockerfile` + `nginx.conf`).
- Single self-contained `index.html` (inline CSS/JS, OKLCH tokens, Geist + Geist Mono).
- Vendored, no CDN: Turndown + GFM plugin, marked, DOMPurify, fonts — all under `vendor/`.
- Served by `nginx:alpine` (`Dockerfile` + `nginx.conf`) with a service worker for offline use.
- No build step, no backend, no database.
## Lokal starten
## Run locally
```bash
docker build -t clip2md .
docker run --rm -p 8080:80 clip2md
# → http://localhost:8080
docker run --rm -p 8080:80 clip2md # http://localhost:8080
```
Oder ganz ohne Docker: `index.html` einfach im Browser öffnen.
Or just open `index.html` in a browser (the service worker only registers over http/https).
## Deployment
## Self-host
Deployt über **Coolify** (Dockerfile-Build) aus diesem Gitea-Repo, Domain via Cloudflare
auf `clip2md.heidrich-digital.de`. Details siehe Outline-Wiki (Tech & Stack → clip2md).
Any static host works — point it at this repo. The reference deployment uses
[Coolify](https://coolify.io) with a Dockerfile build and a reverse proxy for TLS.
## Vendor-Libs aktualisieren
## Update vendored libraries
```bash
npm pack turndown turndown-plugin-gfm
# dist/turndown.js vendor/turndown.js
# dist/turndown-plugin-gfm.js vendor/turndown-plugin-gfm.js
npm pack turndown turndown-plugin-gfm marked dompurify geist
# turndown/dist/turndown.js -> vendor/turndown.js
# turndown-plugin-gfm/dist/turndown-plugin-gfm.js -> vendor/turndown-plugin-gfm.js
# marked/lib/marked.umd.js -> vendor/marked.umd.js
# dompurify/dist/purify.min.js -> vendor/purify.min.js
# geist/dist/fonts/**/*-Variable.woff2 -> vendor/fonts/
```
Lizenz: MIT (Turndown ebenfalls MIT).
Bump the `CACHE` constant in `sw.js` whenever an asset changes so clients pick it up.
## License
[MIT](LICENSE). Turndown, marked, DOMPurify and Geist are under their respective MIT/OFL licenses.
Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<rect width="512" height="512" rx="112" fill="#c14a2b"/>
<text x="50%" y="52%" dominant-baseline="central" text-anchor="middle"
font-family="ui-monospace, Menlo, Consolas, monospace" font-size="230" font-weight="700"
letter-spacing="-12" fill="#fdf6f1">m↓</text>
</svg>

After

Width:  |  Height:  |  Size: 380 B

+533 -528
View File
File diff suppressed because it is too large Load Diff
+13
View File
@@ -0,0 +1,13 @@
{
"name": "clip2md — Paste to Markdown",
"short_name": "clip2md",
"description": "Paste rich text or a web page and save it as clean Markdown in one keystroke. Client-side, private, offline-capable.",
"start_url": ".",
"scope": ".",
"display": "standalone",
"background_color": "#faf7f2",
"theme_color": "#faf7f2",
"icons": [
{ "src": "icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any maskable" }
]
}
+10 -1
View File
@@ -16,12 +16,21 @@ server {
return 200 "ok\n";
}
# Vendor libs can be cached hard.
# Vendor libs + fonts can be cached hard.
location /vendor/ {
expires 30d;
add_header Cache-Control "public, immutable";
}
# Service worker and manifest must always revalidate.
location = /sw.js {
add_header Cache-Control "no-cache";
}
location = /manifest.webmanifest {
default_type application/manifest+json;
add_header Cache-Control "no-cache";
}
# Never cache the app shell, so updates land immediately.
location = /index.html {
add_header Cache-Control "no-cache";
+43
View File
@@ -0,0 +1,43 @@
/* clip2md service worker — offline shell cache. Bump CACHE on any asset change. */
var CACHE = 'clip2md-v2';
var ASSETS = [
'./',
'./index.html',
'./icon.svg',
'./manifest.webmanifest',
'./vendor/turndown.js',
'./vendor/turndown-plugin-gfm.js',
'./vendor/marked.umd.js',
'./vendor/purify.min.js',
'./vendor/fonts/Geist-Variable.woff2',
'./vendor/fonts/GeistMono-Variable.woff2'
];
self.addEventListener('install', function (e) {
self.skipWaiting();
e.waitUntil(caches.open(CACHE).then(function (c) { return c.addAll(ASSETS); }));
});
self.addEventListener('activate', function (e) {
e.waitUntil(
caches.keys().then(function (keys) {
return Promise.all(keys.filter(function (k) { return k !== CACHE; }).map(function (k) { return caches.delete(k); }));
}).then(function () { return self.clients.claim(); })
);
});
self.addEventListener('fetch', function (e) {
if (e.request.method !== 'GET') return;
e.respondWith(
caches.match(e.request).then(function (hit) {
return hit || fetch(e.request).then(function (res) {
// Runtime-cache same-origin successful responses.
if (res && res.ok && e.request.url.indexOf(self.location.origin) === 0) {
var copy = res.clone();
caches.open(CACHE).then(function (c) { c.put(e.request, copy); });
}
return res;
}).catch(function () { return caches.match('./index.html'); });
})
);
});
Binary file not shown.
Binary file not shown.
+80
View File
File diff suppressed because one or more lines are too long
+3
View File
File diff suppressed because one or more lines are too long