mdreader-pwa
English · Español
A standalone, offline-first Markdown reader, installable as a PWA. Single HTML file with no build dependencies, math formula support (KaTeX), diagrams (Mermaid), and integration with the OS so you can open .md files straight from other apps.
Features
- Single HTML file. No build step, no
node_modules, no framework. Open it directly in a browser or serve it as-is.
- Works offline. All Markdown processing happens client-side. Nothing you open ever leaves your machine.
- Math formulas. When online, it loads KaTeX from a CDN for real math typesetting (
$inline$ and $$block$$). Offline, it falls back to a readable Unicode approximation.
- Diagrams.
```mermaid blocks are drawn with Mermaid — flowcharts, sequence, class, state, Gantt. They follow the reader's theme and are redrawn in light colors before printing. If the library can't be loaded, the block stays visible as source code.
- Syntax highlighting. Code blocks tagged with JavaScript, TypeScript, Python, HTML, CSS, Java, C, C#, SQL or Power Query are highlighted with Prism, loaded on demand. Colors come from the reader's own theme. Without a connection, or for any other language, the block is shown in a single color. Every block has a button at its top right corner that copies the whole snippet to the clipboard.
- Bar, line, area, scatter and pie charts. A
```grafico (or ```chart) block turns a small comma-, semicolon- or tab-separated table into a chart, drawn by the reader itself as SVG — no library, so it works offline too. It follows the theme, prints in light colors, shows every value on hover, and has a Ver datos table view. The scatter uses a numeric horizontal axis when the categories are numbers, and the pie labels each slice with its share.
- In-document table of contents. A
[TOC] line is replaced by a linked list of the document's headings. Unlike the side panel, it prints.
- Open entire folders. Browse multiple
.md files from a project in a side panel, without uploading anything to a server.
- Import from a GitHub repo. Point it at
owner/repo (or paste the repo URL) and it pulls every .md/.markdown/.txt file from the latest commit of a branch — no git clone, no history. Private repos work too, with a personal access token you paste once and can choose to remember in the browser. A ↻ Actualizar button re-fetches the latest commit on demand.
- Open files from other apps. On Android, the installed app appears in the system Share sheet — send a
.md from WhatsApp, a file manager, Drive, or Telegram straight into the reader. On desktop (Windows, macOS, Linux, ChromeOS), it registers as a file handler and appears in the "Open with" menu.
- Keeps your workspace. Loaded documents, the selected file, and the reading position are stored locally and restored after closing, restarting, or updating the app. A document stays loaded until you remove it explicitly.
- Updates itself. New deploys reach installed copies automatically — no cache clearing, no reinstalling. See Updates.
- Read aloud. A speaker button in the top bar reads the open document with the device's own voice (the browser's speech synthesis), so nothing is downloaded and it works offline whenever the system has local voices. It starts from your selection or from what is on screen, highlights the block being read, and has pause, previous/next paragraph, speed and voice controls. The voice follows the document's language, not the interface's. Formulas are read the way they are said in maths ("x squared", "integral from 0 to 1 of") and charts are described from their data; code and diagrams are only announced. A voice that sends text to a server is marked (online) and needs a confirmation first.
- Dedicated print styles. A separate
@media print stylesheet so what you read on screen prints cleanly.
- Built-in help. A ? Help button opens a sample document that explains what the reader does and which Markdown it understands. Every feature is shown twice: first the source exactly as it goes in the file, then the rendered result. It travels inside
index.html, so it is there on a first run and on a USB copy with no connection.
- English and Spanish. The interface and the help follow the browser's language, and an EN / ES button in the side panel switches between them. The choice is remembered. Documents themselves are never translated or altered.
Why this exists
Online Markdown readers usually require uploading the file to some service, or are browser extensions tied to a single device. This project came out of a simple need: open a .md file from a phone without uploading it anywhere, even without internet access. The installable PWA version also works around the fact that Android has no app associated with the .md extension by default.
Quick use (no installation needed)
Try it right now at mdreader.jhproyectos.com.ar, or open index.html locally in any modern browser — double-click it, or "Open with Chrome" from the file explorer. From there:
- Open folder — select an entire folder; it lists every
.md/.markdown/.txt file it contains. ↻ Refresh folder picks up what changed since. In desktop Chrome and Edge the reader keeps access to the folder (showDirectoryPicker, stored in IndexedDB) and re-reads it on its own: new files are added, changed ones updated and deleted ones removed. Chrome for Android doesn't have that API, so there it falls back to the classic picker (see known limitations).
- Open individual files — select one or several specific files. Besides Markdown, code files are accepted when you turn on Also open code files in the Open files panel (
.js, .json, .ts, .py, .html, .xml, .css, .java, .c, .cs, .sql, .pq and a few more): they aren't processed as Markdown but shown whole as a single highlighted code block, with the copy button. They open from a computer (picker, drag and drop, folder, or Open with); on a phone they don't appear in the Share menu, and GitHub import brings them too.
- Drag and drop — drop files anywhere on the reading pane.
On narrow screens the side panel collapses; the ☰ button in the top bar opens it, and the same pickers are also available on the empty state, next to See an example, which opens the built-in help.
This mode requires no hosting and no internet connection, and is the simplest option if you only need to read files you pick yourself from within the app.
Import from a GitHub repo
Click Importar de GitHub, paste owner/repo or the repo's URL, and (optionally) a branch — it defaults to the repo's default branch. This does not clone the repository: it reads the file tree of the latest commit through GitHub's API and downloads only the .md, .markdown and .txt files it finds.
- Public repos need nothing else.
- Private repos need a personal access token with read-only access to the repo's contents — the dialog links straight to creating one, scoped to just that repo. The token stays in this browser only, and only if you tick "remember".
- Once imported, a ↻ Actualizar button appears next to the file list: it re-fetches the latest commit, updating changed files, adding new ones, and removing files that no longer exist upstream.
Installable PWA usage (opening .md from other apps)
To integrate with the operating system, the app must be served from an HTTPS origin and installed as a PWA. This is a platform restriction: an HTML file opened via file:// cannot register a service worker or a manifest, and without those the OS has no way to associate it with a file type.
The integration differs by platform, because the underlying browser APIs do:
| Platform | API | Where the app appears |
|---|
| Android | Web Share Target | The system Share sheet |
| Windows / macOS / Linux / ChromeOS | File Handling | The "Open with" menu |
| iOS | — | Not supported (see limitations) |
Deploy
- Clone this repo (or use the "Deploy with Vercel" button above).
- Deploy it to any static hosting provider — Vercel, GitHub Pages, Netlify, etc. No build command is required: it's a fully static site.
- Open the deployed URL in Chrome for Android.
If you deploy somewhere other than Vercel, port the two rules in vercel.json: the Service-Worker-Allowed: / header on sw.js, and a rewrite that sends /share-target to index.html.
Install on your phone
- With the URL open in Chrome, tap the ⋮ menu → "Install app" (or the banner Chrome offers automatically).
- Once installed, long-press a
.md in a WhatsApp chat (or find one in a file manager or Drive) and tap Share.
- Choose Lector MD from the share sheet. The file opens straight in the reader.
If you get many files on WhatsApp, you can also tap Open folder in the reader and pick Internal storage/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Documents. When a new one arrives, tap ↻ Refresh folder and pick the same folder.
On Android the entry point is Share, not "Open with" — Chrome for Android does not implement the File Handling API, so an installed PWA cannot register itself as a handler for a file extension there.
Install on an iPhone or iPad
- Open the URL and tap the browser's Share button → Add to Home Screen.
- Launch it from the home screen: standalone window, its own icon, offline support.
- To read a file, tap Open files and pick your
.md from the Files app. A file someone sent you over WhatsApp or Telegram has to be saved with Save to Files first.
The app detects iOS and adapts on its own: it hides Open folder (webkitdirectory is declared in WebKit but the picker ignores it), drops the accept filter on the file input (the Files picker turns extension lists into system types and ends up greying everything out), promotes Open files to the primary action, and swaps the Android share instructions for the ones above. The detection is user-agent based on purpose — feature detection reports these APIs as present on iOS.
Installing matters more here than on other platforms: WebKit wipes a plain website's IndexedDB and caches after seven days without a visit, and home screen web apps are exempt.
Known limitations
- iOS/Safari supports neither File Handling nor Share Target for files. On iPhone the app can be installed as a PWA (home screen icon, works offline), but the system won't offer it as a way to open
.md files from outside the app. There, the only way in is the Open files picker inside the app itself. Full integration would require a native wrapper (WKWebView plus a declared document type), which this repo does not do.
- Android: Share sheet only, not "Open with". The File Handling API is desktop-only. Getting into the "Open with" menu on Android would require packaging the PWA as a TWA/APK, which this repo does not do.
- Desktop "Open with" requires Chromium ≥102. Firefox and Safari do not implement the File Handling API.
.md MIME types are inconsistent. Android apps report Markdown files under several types, so share_target accepts a broad list including application/octet-stream. The trade-off is that the reader may also appear when sharing unrelated binary files.
- WhatsApp sharing has broken before. In September 2026 (Chrome 153), sharing from a WhatsApp chat delivered only the subject and the caption, never the file. Since October 2026 it works again. If it breaks again, the reader shows a notice with the way around it: download the file in the chat and open the folder where WhatsApp keeps documents,
Internal storage/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Documents, with Open folder; ↻ Refresh folder picks up the ones that arrive later.
- Android: refreshing a folder means picking it again. Chrome for Android only offers the classic folder picker (
webkitdirectory), which hands the page a one-time snapshot and never the folder's path. ↻ Refresh folder reopens that picker, and Chrome asks every time whether to upload all the folder's files, counting everything in it, not just the .md. Neither can be avoided from the page.
- Google Docs named
.md arrive as PDF. If Drive converted an uploaded .md into a Google Doc, Drive delivers it as a PDF. The reader detects it and doesn't open it; the fix is File → Download → Markdown (.md) in Docs.
- Read aloud is only as good as the device's voices, and may stop in the background. Voice quality, accent and available languages come from the operating system and browser, so they differ between Windows, macOS, Android and iOS. iOS and Android can cut speech when the app goes to the background or the screen locks: the reader asks for a screen wake lock while reading and, if the system cuts the voice anyway, pauses and resumes from the same sentence when you return. Diagrams are only announced, not described. Formulas are read from their TeX with a built-in set of rules that covers the common cases (powers, fractions, roots, sums, integrals, limits, derivatives, matrices); anything it doesn't understand is just called .
Updates
Installed copies update on their own without interrupting an open document. The mechanism has four parts:
sw.js serves the HTML network-first. With a connection, the server's version always wins; the cache is only an offline fallback. This is what prevents a stale version from being frozen on a device forever. Icons and the manifest use stale-while-revalidate — instant from cache, refreshed in the background.
- The page checks for a new service worker on load and every time the app becomes visible again, which is the normal usage pattern for an installed PWA.
- A new worker waits while the current app is open. It is downloaded in the background but does not take control or reload the page automatically.
- The workspace lives in IndexedDB, outside the service-worker cache. If you choose Update now, the app first stores every loaded document, the selected file, and its scroll position. It reloads only after that write succeeds and restores the same workspace immediately. If you do nothing, the update is applied the next time the app starts.
Releasing a new version
Bump VERSION at the top of sw.js and deploy. The new version is installed in the background and remains waiting while the app is in use. Once activated, its activate handler deletes older lector-md-* shell caches automatically:
const VERSION = "v0.10.0";
The share-target cache and the IndexedDB workspace are excluded from that cleanup. Therefore, deleting an old application cache never removes a loaded document. The workspace is cleared only with Clear all, by removing documents individually, or by clearing the site's browser data.
Project structure
├── index.html # The full app: HTML + CSS + JS in a single file
├── ejemplo.md # Sample document in Spanish: manual and live demo of the supported syntax
├── example.md # The same sample document in English
├── ejemplo-portada.png # Illustration shown at the top of that document
├── manifest.json # PWA metadata, file_handlers and share_target
├── sw.js # Service worker: caching, updates, share-target handling
├── voz-worker.js # Optional enhanced voice (Supertonic 2) in a Web Worker
├── icon-192.png # App icon (192×192)
├── icon-512.png # App icon (512×512)
├── icon-maskable-512.png # "Maskable" variant (Android may crop it into different shapes)
└── vercel.json # Headers and the /share-target rewrite for Vercel
index.html is fully self-contained: you can copy just that one file — onto a USB stick, into an email — and it works on any machine with a browser. The other files are only needed for the installable, OS-integrated mode.
Technical notes
-
Shared files travel through a cache. Android delivers a shared file as a POST to /share-target, but static hosting can't accept a POST. The service worker intercepts it, writes the file into a short-lived cache, and redirects to the app, which picks it up and empties the cache. The manifest also declares title and text: apps like WhatsApp send a subject and a caption, and without those params Chrome turns them into a fake "shared text" file that looks like the document. The text is only opened as a document when no file arrives and it looks like content (several lines, or long); a bare file name only triggers a notice. A file without a .md extension gets one added.
-
The open workspace is stored locally in IndexedDB. Markdown contents are never sent to the server. The browser may remove them only if the user clears site data or the device removes site storage; the app requests persistent storage when the browser supports it.
-
The maskable icon has its content scaled to 80% and centered, inside the "safe zone" Android respects when cropping icons into different shapes (circle, squircle, etc. depending on the manufacturer).
-
Opening a file from the OS reuses the open window. file_handlers declares "launch_type": "single-client", which only groups several files opened together into one window. What keeps a second .md from opening a second window is "launch_handler": {"client_mode": "focus-existing"}: the window that's already open comes to the front and receives the file through launchQueue, and the reader shows it while the previous files stay in the list. Desktop only (Chrome and Edge 110+); with no window open, or in a browser that ignores it, the app opens a new window. On Windows, installing from Edge is recommended: the .md files then show the reader's icon.
-
KaTeX is optional by design. It loads from a CDN and the app degrades to a Unicode approximation if the request fails, so the reader never depends on a network call to render a document.
-
Mermaid is loaded on demand. The library weighs about 3 MB, so it is only requested when the open document actually contains a diagram; after that the service worker caches it and diagrams keep working offline. The parser emits the source inside a <pre> and the SVG replaces it once drawn, which makes "no library" and "invalid diagram" the same, already-readable fallback.
-
Prism is loaded on demand, in manual mode. The core bundle plus the seven extra grammars weigh about 38 KB, requested only when the open document has a code block in a supported language. Prism only tags tokens with classes; the colors are the reader's own CSS variables, so highlighting follows the theme without a third-party stylesheet and prints in light colors. Once highlighted, a search can't match across two differently colored tokens, the same way it can't across bold and plain text.
License
MIT — use it, modify it, and adapt it to whatever you need.
mdreader-pwa (Español)
English · Español
Lector de archivos Markdown standalone, offline-first e instalable como PWA. Un solo archivo HTML sin dependencias de build, con soporte de fórmulas matemáticas (KaTeX), diagramas (Mermaid) e integración con el sistema operativo para abrir .md desde otras apps.
Características
- Un solo archivo HTML. Sin paso de build, sin
node_modules, sin framework. Se puede abrir directo en el navegador o servir como está.
- Funciona sin conexión. Todo el procesamiento de Markdown ocurre en el cliente. Nada de lo que abrís sale de tu máquina.
- Fórmulas matemáticas. Si hay conexión, carga KaTeX desde CDN para tipografía matemática real (
$inline$ y $$bloque$$). Sin conexión, cae a una aproximación en Unicode legible.
- Diagramas. Los bloques
```mermaid se dibujan con Mermaid: flujos, secuencias, clases, estados, Gantt. Siguen el tema del lector y se redibujan en claro antes de imprimir. Si la librería no se puede cargar, el bloque queda a la vista como código fuente.
- Resaltado de código. Los bloques con lenguaje JavaScript, TypeScript, Python, HTML, CSS, Java, C, C#, SQL o Power Query se resaltan con Prism, que se carga a demanda. Los colores son los del tema del lector. Sin conexión, o con otro lenguaje, el bloque se ve en un solo color. Cada bloque tiene arriba a la derecha un botón que lo copia entero al portapapeles.
- Gráficos de barras, líneas, área, dispersión y torta. Un bloque
```grafico (o ```chart) convierte una tabla chica, separada por comas, punto y coma o tabulaciones, en un gráfico que dibuja el propio lector en SVG: sin librerías, así que funciona también sin conexión. Sigue el tema, se imprime en claro, muestra los valores al pasar el mouse y tiene una vista de tabla en Ver datos. La dispersión usa eje horizontal numérico cuando las categorías son números, y la torta muestra el porcentaje de cada porción.
- Índice dentro del documento. Una línea
[TOC] se reemplaza por la lista de encabezados con enlaces a cada sección. A diferencia del panel lateral, sale impresa.
- Abrir carpetas completas. Navegá varios
.md de un proyecto desde un panel lateral, sin subir nada a un servidor.
- Importar un repo de GitHub. Apuntalo a
usuario/repo (o pegá la URL) y trae todos los .md/.markdown/.txt del último commit de una rama — sin clonar, sin historial. También funciona con repos privados, con un personal access token que se pega una vez y se puede recordar en el navegador. Un botón vuelve a traer el último commit cuando quieras.
Por qué existe
Los lectores de Markdown online suelen requerir subir el archivo a algún servicio, o son extensiones de navegador atadas a un solo dispositivo. Este proyecto nació de una necesidad simple: abrir un .md desde el celular sin subirlo a ningún lado, aunque no haya internet. La versión instalable además resuelve que Android no tenga, por defecto, ninguna app asociada a la extensión .md.
Uso rápido (sin instalar nada)
Probalo ahora mismo en mdreader.jhproyectos.com.ar, o abrí index.html localmente en cualquier navegador moderno — doble clic, o "Ver con Chrome" desde el explorador de archivos. Desde ahí:
- Abrir carpeta — selecciona una carpeta completa; lista todos los
.md/.markdown/.txt que contenga. ↻ Actualizar carpeta suma lo que cambió desde entonces. En Chrome y Edge de escritorio el lector guarda el acceso a la carpeta (showDirectoryPicker, en IndexedDB) y la relee solo: suma los nuevos, actualiza los que cambiaron y saca los borrados. Chrome para Android no tiene esa API, así que ahí usa el selector clásico (ver limitaciones conocidas).
- Abrir archivos sueltos — selecciona uno o varios archivos puntuales. Además de Markdown se aceptan archivos de código cuando activás Abrir también archivos de código en el panel Abrir archivos (
.js, .json, .ts, .py, .html, .xml, .css, .java, .c, .cs, .sql, .pq y algunos más): no se procesan como Markdown, se muestran completos como un único bloque de código resaltado, con el botón de copiar. Se abren desde la computadora (selector, arrastrar y soltar, carpeta o Abrir con); en el celular no aparecen en el menú Compartir, y la importación de GitHub también los trae.
- Arrastrar y soltar — soltá los archivos sobre el panel de lectura.
En pantallas angostas el panel lateral se pliega; el botón ☰ de la barra superior lo abre, y los mismos selectores están también en la pantalla de inicio, junto a Ver un ejemplo, que abre la ayuda incorporada.
Esta forma de uso no requiere hosting, no requiere conexión, y es la más simple si solo necesitás leer archivos que ya elegís vos mismo desde la app.
Importar un repo de GitHub
Tocá Importar de GitHub, pegá usuario/repo o la URL del repo, y opcionalmente una rama (si no ponés nada, usa la rama por defecto). Esto no clona el repositorio: lee el árbol de archivos del último commit a través de la API de GitHub y descarga sólo los .md, .markdown y .txt que encuentra.
- Repos públicos no necesitan nada más.
- Repos privados necesitan un personal access token con acceso de solo lectura al contenido del repo — el diálogo trae un enlace directo para crear uno, limitado a ese repositorio. El token queda solo en este navegador, y solo si tildás "recordar".
- Una vez importado, aparece un botón ↻ Actualizar junto a la lista de archivos: vuelve a traer el último commit, actualiza lo que cambió, agrega lo nuevo y saca lo que ya no está en el repo.
Uso como PWA instalable (abrir .md desde otras apps)
Para integrarse con el sistema operativo, la app tiene que estar servida desde un origen HTTPS e instalada como PWA. Esto es una restricción de la plataforma: un archivo HTML abierto en file:// no puede registrar un service worker ni un manifest, y sin eso el sistema no tiene forma de asociarlo a un tipo de archivo.
La integración cambia según la plataforma, porque las APIs del navegador son distintas:
| Plataforma | API | Dónde aparece la app |
|---|
| Android | Web Share Target | El menú Compartir del sistema |
| Windows / macOS / Linux / ChromeOS | File Handling | El menú "Abrir con" |
| iOS | — | No soportado (ver limitaciones) |
Desplegar
- Cloná este repo (o usá el botón "Deploy with Vercel" de arriba).
- Desplegalo en cualquier hosting estático — Vercel, GitHub Pages, Netlify, etc. No requiere build command: es un sitio 100% estático.
- Abrí la URL desplegada en Chrome para Android.
Si lo desplegás fuera de Vercel, replicá las dos reglas de vercel.json: el header Service-Worker-Allowed: / sobre sw.js, y un rewrite que mande /share-target a index.html.
Instalar en el celular
- Con la URL abierta en Chrome, tocá el menú ⋮ → "Instalar app" (o el banner que ofrece Chrome automáticamente).
- Una vez instalada, mantené apretado un
.md en un chat de WhatsApp (o buscalo en el explorador de archivos o en Drive) y tocá Compartir.
- Elegí Lector MD en el menú de compartir. El archivo se abre directo en el lector.
Si te mandan muchos archivos por WhatsApp, también podés tocar Abrir carpeta en el lector y elegir Almacenamiento interno/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Documents. Cuando llegue uno nuevo, tocá ↻ Actualizar carpeta y elegí la misma carpeta.
En Android el punto de entrada es Compartir, no "Abrir con": Chrome para Android no implementa la File Handling API, así que una PWA instalada no puede registrarse como handler de una extensión de archivo.
Instalar en iPhone o iPad
- Abrí la URL y tocá el botón Compartir del navegador → Agregar a inicio.
- Abrila desde la pantalla de inicio: ventana propia, ícono propio, funciona sin internet.
- Para leer un archivo, tocá Abrir archivos y elegí el
.md desde la app Archivos. Si te lo mandaron por WhatsApp o Telegram, primero hay que guardarlo con Guardar en Archivos.
La app detecta iOS y se acomoda sola: esconde Abrir carpeta (webkitdirectory existe en WebKit pero el selector lo ignora), le saca el filtro accept al input de archivos (el selector de Archivos traduce las extensiones a tipos del sistema y termina mostrando todo en gris), deja Abrir archivos como acción principal y cambia las instrucciones de Android por las de arriba. La detección va por user agent a propósito: por capacidades, iOS declara esas APIs como presentes.
Acá instalarla pesa más que en otras plataformas: WebKit borra el IndexedDB y los cachés de un sitio común a los siete días sin visitas, y las apps agregadas a la pantalla de inicio quedan exentas.
Limitaciones conocidas
- iOS/Safari no soporta ni File Handling ni Share Target para archivos. En iPhone la app se puede instalar como PWA (ícono en el home, funciona offline), pero el sistema no la va a ofrecer para abrir
.md desde fuera de la app. Ahí la única entrada es el selector Abrir archivos dentro de la propia app. Para integrarla de verdad haría falta una envoltura nativa (WKWebView más un document type declarado), cosa que este repo no hace.
- Android: sólo menú Compartir, no "Abrir con". La File Handling API es exclusiva de escritorio. Entrar al menú "Abrir con" en Android requeriría empaquetar la PWA como TWA/APK, cosa que este repo no hace.
- El "Abrir con" de escritorio requiere Chromium ≥102. Firefox y Safari no implementan la File Handling API.
- Los MIME types de
.md son inconsistentes. Las apps de Android reportan los Markdown con varios tipos distintos, así que share_target acepta una lista amplia que incluye application/octet-stream. El costo es que el lector puede aparecer también al compartir otros archivos binarios.
- Compartir desde WhatsApp ya falló una vez. En septiembre de 2026 (Chrome 153), al compartir desde un chat de WhatsApp sólo llegaban el asunto y el epígrafe, nunca el archivo. Desde octubre de 2026 funciona de nuevo. Si vuelve a fallar, el lector muestra un aviso con la salida: descargar el archivo en el chat y abrir con Abrir carpeta la carpeta donde WhatsApp guarda los documentos,
Almacenamiento interno/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp Documents; ↻ Actualizar carpeta suma los que lleguen después.
- Android: actualizar una carpeta es volver a elegirla. Chrome para Android sólo ofrece el selector de carpetas clásico (
webkitdirectory), que le entrega a la página una foto de una sola vez y nunca la ruta de la carpeta. ↻ Actualizar carpeta vuelve a abrir ese selector, y Chrome pregunta cada vez si cargar todos los archivos de la carpeta, contando todo lo que hay adentro y no sólo los .md. Ninguna de las dos cosas se puede evitar desde la página.
- Los documentos de Google con nombre
.md llegan como PDF. Si Drive convirtió un .md subido en un documento de Google, lo entrega como PDF. El lector lo detecta y no lo abre; la solución es Archivo → Descargar → Markdown (.md) en Documentos.
- Escuchar vale lo que valgan las voces del dispositivo, y puede cortarse en segundo plano. La calidad, el acento y los idiomas disponibles los pone el sistema operativo y el navegador, así que cambian entre Windows, macOS, Android e iOS. iOS y Android pueden cortar la voz cuando la app pasa a segundo plano o se bloquea la pantalla: el lector pide mantener la pantalla encendida mientras lee y, si el sistema igual la corta, queda en pausa y sigue desde la misma oración al volver. Los diagramas sólo se anuncian, no se describen. Las fórmulas se leen a partir de su TeX con un conjunto de reglas propio que cubre lo habitual (potencias, fracciones, raíces, sumatorias, integrales, límites, derivadas, matrices); lo que no entiende se dice sólo .
Actualizaciones
Las copias instaladas se actualizan solas sin interrumpir un documento abierto. El mecanismo tiene cuatro partes:
sw.js sirve el HTML network-first. Con conexión siempre gana la versión del servidor; el caché es solo respaldo offline. Esto es lo que evita que una versión vieja quede congelada para siempre en un dispositivo. Los íconos y el manifest usan stale-while-revalidate: instantáneos desde el caché, refrescados en segundo plano.
- La página busca un service worker nuevo al cargar y cada vez que la app vuelve a estar visible, que es el patrón de uso normal de una PWA instalada.
- Un worker nuevo espera mientras la app actual está abierta. Se descarga en segundo plano, pero no toma control ni recarga la página automáticamente.
- El área de trabajo vive en IndexedDB, fuera del caché del service worker. Si elegís Actualizar ahora, la app guarda primero todos los documentos cargados, el archivo seleccionado y su posición. Sólo recarga si ese guardado termina correctamente y restaura inmediatamente la misma sesión. Si no hacés nada, la actualización se aplica la próxima vez que abras la app.
Publicar una versión nueva
Subí VERSION arriba de todo en sw.js y desplegá. La versión nueva se instala en segundo plano y queda en espera mientras la app esté en uso. Una vez activada, su handler de activate elimina automáticamente los cachés de shell lector-md-* anteriores:
const VERSION = "v0.10.0";
El caché del share target y el área de trabajo guardada en IndexedDB quedan fuera de esa limpieza. Por eso borrar un caché viejo de la aplicación nunca elimina un documento cargado. El área de trabajo sólo se vacía con Limpiar todo, quitando cada documento o borrando los datos del sitio desde el navegador.
Estructura del proyecto
├── index.html # La app completa: HTML + CSS + JS en un solo archivo
├── ejemplo.md # Documento de muestra en español: manual y demo viva de la sintaxis soportada
├── example.md # El mismo documento de muestra, en inglés
├── ejemplo-portada.png # Ilustración que encabeza ese documento
├── manifest.json # Metadata de PWA, file_handlers y share_target
├── sw.js # Service worker: caché, actualizaciones y share target
├── voz-worker.js # Voz mejorada opcional (Supertonic 2) en un Web Worker
├── icon-192.png # Ícono de la app (192×192)
├── icon-512.png # Ícono de la app (512×512)
├── icon-maskable-512.png # Variante "maskable" (Android puede recortarla en distintas formas)
└── vercel.json # Headers y el rewrite de /share-target para Vercel
index.html es completamente autocontenido: podés copiar únicamente ese archivo —a un pendrive, a un mail— y funciona en cualquier máquina con navegador. Los demás archivos solo hacen falta para el modo instalable e integrado al sistema.
Notas técnicas
-
Los archivos compartidos viajan por un caché. Android entrega el archivo compartido como un POST a /share-target, pero un hosting estático no puede recibir POST. El service worker lo intercepta, escribe el archivo en un caché de vida corta y redirige a la app, que lo levanta y vacía el caché. El manifest también declara title y text: apps como WhatsApp mandan un asunto y un epígrafe, y sin esos parámetros Chrome los convierte en un archivo falso ("texto compartido") que parece el documento. El texto sólo se abre como documento si no llegó ningún archivo y tiene pinta de contenido (varias líneas, o largo); un nombre de archivo suelto sólo muestra un aviso. A un archivo sin extensión .md se le agrega.
-
El área de trabajo abierta se guarda localmente en IndexedDB. El contenido Markdown nunca se envía al servidor. Sólo puede desaparecer si el usuario borra los datos del sitio o si el dispositivo elimina ese almacenamiento; la app solicita almacenamiento persistente cuando el navegador lo permite.
-
El ícono maskable tiene el contenido escalado al 80% y centrado, dentro de la "zona segura" que Android respeta al recortar los íconos en distintas formas (círculo, squircle, etc. según el fabricante).
-
Abrir un archivo desde el sistema reutiliza la ventana abierta. file_handlers declara "launch_type": "single-client", que solo agrupa en una misma ventana varios archivos abiertos juntos. Lo que evita que un segundo .md abra una segunda ventana es "launch_handler": {"client_mode": "focus-existing"}: la ventana que ya está abierta pasa al frente y recibe el archivo por launchQueue, y el lector lo muestra mientras los anteriores quedan en la lista. Solo en escritorio (Chrome y Edge 110 o superior); si no hay ninguna ventana abierta, o en un navegador que lo ignore, la app abre una ventana nueva. En Windows se recomienda instalarla desde Edge: así los .md muestran el ícono del lector.
-
KaTeX es opcional por diseño. Se carga desde CDN y la app degrada a una aproximación en Unicode si el pedido falla, así el lector nunca depende de una llamada de red para mostrar un documento.
-
Mermaid se carga a demanda. La librería pesa unos 3 MB, así que se pide recién cuando el documento abierto tiene algún diagrama; después el service worker la cachea y los diagramas siguen funcionando sin conexión. El parser deja el código fuente en un <pre> y el SVG lo reemplaza una vez dibujado, de modo que "sin librería" y "diagrama inválido" caen en el mismo respaldo, que ya es legible.
Licencia
MIT — usalo, modificalo, y adaptalo a lo que necesites.