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:
+1
-1
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||

|
||||
|
||||
- **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 |
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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";
|
||||
|
||||
@@ -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'); });
|
||||
})
|
||||
);
|
||||
});
|
||||
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
Vendored
+80
File diff suppressed because one or more lines are too long
Vendored
+3
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user