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.
|
# Static single-page app — no build step.
|
||||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
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/
|
COPY vendor/ /usr/share/nginx/html/vendor/
|
||||||
|
|
||||||
EXPOSE 80
|
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
|
# clip2md — Paste to Markdown
|
||||||
|
|
||||||
Winziges, self-hosted Tool: langen Text (Reddit-Thread, Website, formatierter Content) reinpasten
|
Paste a long Reddit thread, an article, or any formatted web content, and save it as clean
|
||||||
→ ein Klick / `⌘/Strg+S` → als `.md` speichern. Dateiname wird automatisch aus Titel + Datum vorgeschlagen.
|
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
|
## Why
|
||||||
`⌘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.
|
|
||||||
|
|
||||||
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
|
## Tech
|
||||||
|
|
||||||
- Statische `index.html` (inline CSS/JS, OKLCH-Design, Dark).
|
- Single self-contained `index.html` (inline CSS/JS, OKLCH tokens, Geist + Geist Mono).
|
||||||
- Vendored: `vendor/turndown.js`, `vendor/turndown-plugin-gfm.js` (kein CDN, offline-fähig).
|
- Vendored, no CDN: Turndown + GFM plugin, marked, DOMPurify, fonts — all under `vendor/`.
|
||||||
- Ausgeliefert via `nginx:alpine` (siehe `Dockerfile` + `nginx.conf`).
|
- 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
|
```bash
|
||||||
docker build -t clip2md .
|
docker build -t clip2md .
|
||||||
docker run --rm -p 8080:80 clip2md
|
docker run --rm -p 8080:80 clip2md # http://localhost:8080
|
||||||
# → 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
|
Any static host works — point it at this repo. The reference deployment uses
|
||||||
auf `clip2md.heidrich-digital.de`. Details siehe Outline-Wiki (Tech & Stack → clip2md).
|
[Coolify](https://coolify.io) with a Dockerfile build and a reverse proxy for TLS.
|
||||||
|
|
||||||
## Vendor-Libs aktualisieren
|
## Update vendored libraries
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm pack turndown turndown-plugin-gfm
|
npm pack turndown turndown-plugin-gfm marked dompurify geist
|
||||||
# dist/turndown.js → vendor/turndown.js
|
# turndown/dist/turndown.js -> vendor/turndown.js
|
||||||
# dist/turndown-plugin-gfm.js → vendor/turndown-plugin-gfm.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 |
+546
-541
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";
|
return 200 "ok\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
# Vendor libs can be cached hard.
|
# Vendor libs + fonts can be cached hard.
|
||||||
location /vendor/ {
|
location /vendor/ {
|
||||||
expires 30d;
|
expires 30d;
|
||||||
add_header Cache-Control "public, immutable";
|
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.
|
# Never cache the app shell, so updates land immediately.
|
||||||
location = /index.html {
|
location = /index.html {
|
||||||
add_header Cache-Control "no-cache";
|
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