Node.js · Vercel Functions · JavaScript
Markdown Notes
A notes app where Markdown is rendered to HTML entirely on the server by a hand-written parser — no client-side library.
Type Markdown on the left, see it rendered on the right — but the rendering happens on the server. The backend ships a small hand-written Markdown-to-HTML converter (headings, bold/italic, lists, links, code blocks, blockquotes) instead of pulling in a client-side Markdown library.
Live demo
embedfunction escapeHtml(s) {
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
}
function inline(text) {
let out = escapeHtml(text);
out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
out = out.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
out = out.replace(/\*([^*]+)\*/g, '<em>$1</em>');
out = out.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" rel="noopener">$1</a>');
return out;
}
function renderMarkdown(markdown) {
const lines = (markdown || '').replace(/\r\n/g, '\n').split('\n');
const html = [];
let inCode = false;
let listOpen = false;
let quoteOpen = false;
const closeList = () => { if (listOpen) { html.push('</ul>'); listOpen = false; } };
const closeQuote = () => { if (quoteOpen) { html.push('</blockquote>'); quoteOpen = false; } };
for (const raw of lines) {
if (raw.trim().startsWith('```')) {
if (!inCode) { closeList(); closeQuote(); html.push('<pre><code>'); inCode = true; }
else { html.push('</code></pre>'); inCode = false; }
continue;
}
if (inCode) { html.push(escapeHtml(raw)); continue; }
const heading = raw.match(/^(#{1,6})\s+(.*)$/);
if (heading) {
closeList(); closeQuote();
const level = heading[1].length;
html.push(`<h${level}>${inline(heading[2])}</h${level}>`);
continue;
}
if (/^>\s?/.test(raw)) {
closeList();
if (!quoteOpen) { html.push('<blockquote>'); quoteOpen = true; }
html.push(`<p>${inline(raw.replace(/^>\s?/, ''))}</p>`);
continue;
}
closeQuote();
if (/^\s*[-*]\s+/.test(raw)) {
if (!listOpen) { html.push('<ul>'); listOpen = true; }
html.push(`<li>${inline(raw.replace(/^\s*[-*]\s+/, ''))}</li>`);
continue;
}
closeList();
if (raw.trim() === '') continue;
html.push(`<p>${inline(raw)}</p>`);
}
closeList(); closeQuote();
if (inCode) html.push('</code></pre>');
return html.join('\n');
}
module.exports = { renderMarkdown };
const editor = document.getElementById('editor');
const preview = document.getElementById('preview');
const titleInput = document.getElementById('title');
const saveBtn = document.getElementById('save');
const list = document.getElementById('list');
let debounceTimer = null;
async function renderPreview() {
const res = await fetch('/api/render', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ markdown: editor.value }),
});
const data = await res.json();
preview.innerHTML = data.html;
}
editor.addEventListener('input', () => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(renderPreview, 250);
});
async function loadNotes() {
const res = await fetch('/api/notes');
const notes = await res.json();
list.innerHTML = notes.map((n) => `
<div class="card" style="margin-top:10px">
<strong>${n.title}</strong>
<div class="preview" style="margin-top:8px">${n.html}</div>
</div>
`).join('') || '<p style="color:var(--muted)">No notes saved yet.</p>';
}
saveBtn.addEventListener('click', async () => {
await fetch('/api/notes', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: titleInput.value, markdown: editor.value }),
});
loadNotes();
});
renderPreview();
loadNotes();