TL;DR: You will use AI to build a markdown notes app in a single HTML file. It supports creating, editing, and deleting notes, renders markdown to formatted HTML in real time, saves everything to localStorage so nothing is lost when you close the browser, and includes search to find notes instantly. The whole thing takes four prompts and about 30 minutes. You will walk away understanding localStorage, DOM manipulation, third-party library integration, and event handling — the building blocks of every front-end project AI will generate for you.

What You'll Build

By the end of this project you will have a working notes application with these features:

  • Create, edit, and delete notes — full CRUD operations, all running in the browser
  • Markdown editing with live preview — type markdown on one side, see the formatted result on the other
  • Automatic saving — every change is saved to localStorage, so your notes survive browser closes and computer restarts
  • Search and filter — type a keyword and instantly filter your notes list
  • Clean, responsive UI — looks good on desktop and mobile without any extra CSS frameworks

No backend. No database. No hosting. One HTML file you can open directly in your browser and start using immediately. This is the kind of project that teaches you real skills while giving you something you will actually reach for when you need to jot down ideas, save code snippets, or draft project plans.

Why This Project

Most AI coding tutorials have you build things you will never touch again. A random calculator. A to-do list you close and forget. This notes app is different because you will actually use it.

More importantly, it teaches you the four concepts that show up in nearly every project AI generates:

  • localStorage — how browsers save data without a server. You will see this in every app that remembers your preferences, saves form data, or works offline.
  • DOM manipulation — how JavaScript creates, changes, and removes elements on the page. Every interactive UI your AI builds uses this.
  • Third-party libraries — how to pull in someone else's code (marked.js for markdown, DOMPurify for security) instead of writing everything from scratch. AI loves recommending libraries. Knowing how they plug in helps you evaluate those recommendations.
  • Event handling — how buttons, inputs, and keyboard actions trigger code. The glue that makes any UI interactive.

Once you understand these four concepts in the context of this project, you will recognize them instantly in every other project AI builds for you. They are the recurring patterns of front-end development.

The Prompts

Here is the strategy: do not ask AI for the entire app in one giant prompt. Build incrementally. Each prompt adds a layer. This way you can understand what each piece does before the next one lands on top of it.

Prompt 1 — The Foundation

Build a markdown notes app in a single HTML file. Include all CSS and
JavaScript inline. It should have:

1. A sidebar on the left showing a list of saved notes (title + date)
2. An editor area on the right with a textarea for writing
3. A preview panel that shows the markdown rendered as HTML
4. Buttons to create a new note and delete the current note
5. Use the marked.js library from CDN for markdown rendering

Make it look clean and modern — dark theme, good spacing, readable
fonts. No frameworks, just vanilla HTML/CSS/JS.

Comment every major section of the JavaScript so I can understand
what each part does.

Prompt 2 — Persistence

Now add localStorage persistence to the notes app. Every time a note
is created, edited, or deleted, save the entire notes array to
localStorage. When the page loads, read from localStorage and restore
all saved notes.

Handle the case where localStorage is empty (first visit) and where
the stored JSON is corrupted or invalid. Show me what error handling
you added and explain why each check matters.

Prompt 3 — Search

Add a search bar at the top of the sidebar. As I type, filter the
notes list to show only notes whose title or content contains the
search text. The filtering should happen in real time as I type
(not on enter). Make the search case-insensitive.

Keep the currently selected note visible in the editor even if it
does not match the search. Only filter the sidebar list.

Prompt 4 — Security and Polish

Add DOMPurify from CDN to sanitize the markdown HTML output before
inserting it into the preview panel. This prevents XSS attacks from
any markdown content.

Also add:
- Auto-save as I type (debounced, not on every keystroke)
- A "last edited" timestamp on each note
- Keyboard shortcut: Ctrl+N / Cmd+N to create a new note
- Mobile responsive layout (sidebar collapses on small screens)

Explain what DOMPurify does and why it matters for security.

Four prompts. Each one builds on the last. By the time you are done, you have a full-featured notes app — and you understand every layer because you watched them get added one at a time.

What AI Generated

Let us walk through the key sections of code your AI will produce. You do not need to memorize this — you need to recognize what each piece does so you can debug it, extend it, and know when AI has made a mistake.

The note data structure

// Each note is a plain JavaScript object
// All notes live in an array stored in memory and synced to localStorage
let notes = [];
let activeNoteId = null;

function createNote() {
  const note = {
    id: Date.now(),           // Unique ID using current timestamp
    title: 'Untitled Note',   // Default title — user will overwrite
    content: '',              // The raw markdown text
    createdAt: new Date().toISOString(),  // When it was first created
    updatedAt: new Date().toISOString()   // When it was last edited
  };
  notes.unshift(note);   // Add to the beginning of the array (newest first)
  activeNoteId = note.id;
  saveToLocalStorage();  // Persist immediately
  renderNotesList();     // Update the sidebar
  renderEditor();        // Show the new note in the editor
}

This is the heart of the app. Every note is a JSON-friendly object — plain data with no special classes or prototypes. The id uses Date.now() which returns the current time in milliseconds. It is not a perfect unique ID system for big applications, but for a local notes app with one user, it is more than sufficient.

Markdown rendering with marked.js

// marked.js converts markdown text into HTML
// DOMPurify cleans that HTML to prevent XSS attacks

function renderPreview(markdownText) {
  // Step 1: Convert markdown to HTML
  const rawHTML = marked.parse(markdownText);

  // Step 2: Sanitize the HTML — remove any dangerous scripts or attributes
  const cleanHTML = DOMPurify.sanitize(rawHTML);

  // Step 3: Insert the safe HTML into the preview panel
  document.getElementById('preview').innerHTML = cleanHTML;
}

Two libraries doing two jobs. marked.parse() turns your markdown text into HTML — headings, bold text, code blocks, links, all of it. Then DOMPurify.sanitize() strips out anything dangerous before it goes into the page. Why both? Because markdown can contain raw HTML, and raw HTML can contain <script> tags or malicious event handlers. More on this in the security section below.

localStorage save and load

// Save all notes to localStorage as a JSON string
function saveToLocalStorage() {
  try {
    localStorage.setItem('markdown-notes', JSON.stringify(notes));
  } catch (error) {
    // localStorage can fail if storage is full (~5MB limit)
    // or if the browser blocks it (incognito mode in some browsers)
    console.error('Failed to save notes:', error);
  }
}

// Load notes from localStorage when the app starts
function loadFromLocalStorage() {
  try {
    const stored = localStorage.getItem('markdown-notes');
    if (!stored) return []; // First visit — no data yet

    const parsed = JSON.parse(stored);

    // Validate that we got an array back
    if (!Array.isArray(parsed)) {
      console.warn('Stored notes data was not an array — starting fresh');
      return [];
    }

    return parsed;
  } catch (error) {
    // JSON.parse failed — stored data was corrupted
    console.error('Failed to load notes — stored data was invalid:', error);
    return []; // Start with empty notes rather than crashing
  }
}

localStorage only stores strings. That is why you see JSON.stringify() when saving (turning the array of note objects into a string) and JSON.parse() when loading (turning that string back into real objects). The try/catch blocks are critical — without them, one corrupted character in the stored JSON crashes the entire app on load.

Search filtering

// Filter the sidebar list based on search input
function filterNotes(searchTerm) {
  const term = searchTerm.toLowerCase().trim();

  if (!term) {
    // Empty search — show all notes
    renderNotesList(notes);
    return;
  }

  const filtered = notes.filter(note => {
    // Check both title and content (case-insensitive)
    return note.title.toLowerCase().includes(term) ||
           note.content.toLowerCase().includes(term);
  });

  renderNotesList(filtered);
}

// Attach the filter to the search input's "input" event
// This fires on every keystroke, paste, and delete
document.getElementById('search-input')
  .addEventListener('input', (e) => filterNotes(e.target.value));

The input event fires every time the text changes — not just on Enter. The .filter() method creates a new array containing only the notes that match, leaving the original array untouched. The editor keeps showing whatever note you had selected, even if it does not match the search. Only the sidebar list gets filtered.

Auto-save with debouncing

// Debounce — wait until the user stops typing before saving
// Without this, we would save on every single keystroke
let saveTimeout;

function debouncedSave() {
  clearTimeout(saveTimeout);     // Cancel the previous timer
  saveTimeout = setTimeout(() => {
    saveToLocalStorage();        // Save after 300ms of no typing
    updateTimestamp();           // Update the "last edited" time
  }, 300);
}

// Attach to the textarea
document.getElementById('editor-textarea')
  .addEventListener('input', (e) => {
    // Update the note in memory immediately (for preview)
    const note = notes.find(n => n.id === activeNoteId);
    if (note) {
      note.content = e.target.value;
      renderPreview(note.content);  // Update preview in real time
    }
    debouncedSave();  // Save to localStorage after typing pauses
  });

Debouncing is a pattern you will see everywhere in AI-generated code. Without it, typing "hello" would trigger five separate save operations. With it, the save only fires once — 300 milliseconds after you stop typing. The preview still updates on every keystroke (that is cheap), but the localStorage write waits for a pause.

Understanding What AI Built

Single HTML file vs. component approach

AI built everything in one file — HTML, CSS, and JavaScript together. This is not how large applications are structured, but it is perfect for a personal tool. One file means no build step, no dependencies to install, no server to run. You can email it to yourself, drop it on a USB drive, or host it on any static server. The tradeoff is that as the app grows, one file gets unwieldy. If you add enough features that the file exceeds 500 lines, it is time to split into separate files.

Event delegation

If you look at how AI handles clicks on the notes list, it likely used event delegation — attaching one click listener to the parent list element instead of individual listeners to each note item. This is a pattern the DOM encourages because it works even when new notes are added dynamically. If AI attached individual click handlers to each note element, those handlers would break every time the list re-renders.

// Event delegation — one listener handles all note clicks
document.getElementById('notes-list').addEventListener('click', (e) => {
  // Find which note was clicked by walking up the DOM tree
  const noteElement = e.target.closest('[data-note-id]');
  if (!noteElement) return;  // Click was not on a note item

  const noteId = parseInt(noteElement.dataset.noteId);
  selectNote(noteId);
});

Why marked.js?

AI chose marked.js because it is the most popular markdown parsing library — small (under 40KB), fast, well-documented, and loaded from a CDN with one script tag. There are alternatives (showdown, markdown-it, remarkable) but marked.js has the best balance of simplicity and features for a project like this. When AI recommends a library, the important questions are: how big is it, how active is the project, and can I load it from a CDN without a build step?

What AI Gets Wrong

Security Warning

The single most dangerous mistake AI makes in this project is rendering markdown to HTML without sanitization. If you skip DOMPurify, anyone (including you, accidentally) can inject executable JavaScript through markdown. This is a real XSS (cross-site scripting) vulnerability.

XSS from unsanitized markdown

Markdown allows raw HTML. That means if you type <img src=x onerror="alert('hacked')"> in a note, marked.js will pass it straight through as HTML. Without DOMPurify, that HTML gets inserted into the page with innerHTML, and the JavaScript runs. In a personal notes app this is less dangerous — you are only attacking yourself — but it is still a terrible habit. And if you ever extend this app to share notes with others, it becomes a real security hole.

// ❌ What AI sometimes generates — UNSAFE
document.getElementById('preview').innerHTML = marked.parse(markdown);

// ✅ What you should always do — sanitize first
document.getElementById('preview').innerHTML =
  DOMPurify.sanitize(marked.parse(markdown));

This is one line of code that prevents an entire category of attacks. Always use it. Read more about why in our guide on what XSS is and how to prevent it.

Losing data on JSON parse errors

AI's first attempt at loading from localStorage often looks like this:

// ❌ Dangerous — one corrupted character crashes the app
const notes = JSON.parse(localStorage.getItem('markdown-notes'));

If the stored string gets corrupted — which can happen during a browser crash, storage quota overflow, or a bad extension modifying localStorage — JSON.parse() throws an error and your app never loads. The fix is the try/catch pattern shown above. Always wrap JSON.parse() in error handling when you are reading data you did not just write.

No export or import

AI almost never adds export/import functionality unless you ask for it. Your notes are locked inside one browser on one device. If you clear your browser data, switch browsers, or get a new computer, everything is gone. This is the biggest practical limitation of a localStorage-only app, and it is something you should add yourself (covered in the extensions section below).

No confirmation on delete

AI will often wire up the delete button to immediately remove a note with no confirmation dialog. One misclick and a note is gone permanently — localStorage has no undo. Always ask AI to add a confirmation step: "Are you sure you want to delete this note?" It is a one-line fix that prevents real data loss.

Make It Your Own

The base app is functional but basic. Here are three extensions worth building — each one teaches you something new.

1. Dark mode toggle

Add a button that switches between light and dark themes. Save the user's preference to localStorage so it persists. This teaches you CSS custom properties (variables), toggling CSS classes on the document body, and storing user preferences — a pattern used by nearly every modern web app.

Prompt for This Extension

Add a dark/light mode toggle button to the notes app. Use CSS custom
properties for colors so switching themes only requires toggling a
class on the body element. Save the theme preference to localStorage
so it persists between visits. Default to dark mode.

2. Export to .md files

Add a button that downloads the current note as a .md file, and another that downloads all notes as a zip. This teaches you the Blob API, dynamically creating download links, and working with file data in the browser. More importantly, it solves the biggest limitation of localStorage — your data is no longer trapped.

Prompt for This Extension

Add export functionality to the notes app:
1. An "Export Note" button that downloads the current note as a .md
   file using the note title as the filename
2. An "Export All" button that downloads all notes as individual
   .md files inside a zip (use JSZip from CDN)
3. An "Import" button that lets me select .md files from my computer
   and adds them as new notes

Use the Blob API for downloads. No server needed.

3. Categories and tags

Add a way to organize notes with tags or categories. Each note can have one or more tags, and the sidebar gets a filter dropdown to show only notes with a specific tag. This teaches you array filtering with multiple criteria, UI state management, and data modeling — you are adding a new property to your note objects and need to handle notes created before tags existed.

Prompt for This Extension

Add a tagging system to the notes app:
1. Each note can have multiple tags (stored as an array of strings)
2. A tag input below the title that lets me add/remove tags
3. A tag filter in the sidebar — click a tag to show only notes
   with that tag
4. Display tags as colored chips on each note in the sidebar list

Handle backwards compatibility — notes created before this update
won't have a tags property. Default to an empty array.

What You Learned

This project covers the building blocks that show up in almost every front-end project AI generates. Here is what you now understand:

  • localStorage — storing and retrieving JSON data in the browser, handling errors, understanding the 5MB size limit, and knowing when localStorage is the right tool vs. when you need a real database
  • DOM manipulation — creating elements dynamically, updating the page when data changes, inserting HTML safely, and the difference between textContent and innerHTML
  • Third-party library integration — loading libraries from a CDN, understanding what they do, and evaluating whether a library is worth adding to your project
  • Event handling — attaching listeners, event delegation, debouncing, and the input event for real-time filtering
  • Security basics — why rendering unsanitized HTML is dangerous, what XSS means in practice, and how DOMPurify prevents it with one function call

These are not abstract concepts anymore. You have seen them work together in a real application. The next time AI generates code that uses localStorage.setItem(), document.createElement(), or addEventListener(), you will know exactly what is happening — and you will catch the mistakes AI makes.

What to Build Next

Now that you have the fundamentals of localStorage, DOM manipulation, and event handling down, here are three projects that build on those skills:

  • Build a To-Do App with AI — Similar localStorage pattern but adds drag-and-drop reordering, due dates, and priority levels. A great next step that reinforces what you learned here while adding new interaction patterns.
  • Build a Blog with AI — Takes the markdown rendering skills from this project and applies them to a static site generator. You will learn file structure, routing, and templating.
  • Build a Dashboard with AI — Combines DOM manipulation with data visualization. You will pull data from APIs, cache it, and render charts — a meaningful step up in complexity.

Next Step

Open a new file in your code editor, paste that first prompt into Claude or Cursor, and start building. Do not wait until you feel ready — the whole point of building with AI is that you learn by doing. Your notes app will be working in 30 minutes, and you will understand more about front-end development than most tutorials teach in 30 hours.

FAQ

No. This app runs entirely in the browser using localStorage to save your notes. There is no server, no database, and no hosting required beyond a static HTML file. You can open the file directly in your browser and start taking notes immediately. If you later want cloud sync or multi-device access, that is when you would add a backend — but start without one.

Yes. localStorage persists data even after you close the browser or restart your computer. Your notes will be there when you come back. The only ways to lose them are: clearing your browser data (Settings → Clear browsing data), using a different browser or device, or browsing in incognito/private mode (where localStorage is wiped when you close the window).

Markdown is a simple way to format text using plain characters. A # makes a heading. **text** makes bold. - item makes a bullet list. It is faster than clicking formatting buttons like you would in Google Docs, and the plain text is always readable even without rendering. Most developer tools, GitHub, and documentation systems use markdown, so learning it once pays off across dozens of tools you will encounter.

Not with the base version. localStorage is tied to one browser on one device — it has no concept of syncing. To share notes across devices, you would need to add a backend with a database (like Firebase, Supabase, or your own server). A simpler workaround is the export/import feature described in the extensions section: export your notes as .md files on one device and import them on another.

Not for truly sensitive data like passwords, API keys, or financial information. localStorage is accessible to any JavaScript running on the page, which means an XSS vulnerability could expose everything stored there. For personal notes, to-do lists, and project ideas it is perfectly fine. For anything sensitive, you need a backend with proper encryption, authentication, and access controls.