For example, a saved workshop listing might contain a heading, navigation, and several sessions. An import needs session titles and dates. Converting every navigation link and decorative element into a giant tree would leave the import with more cleanup work.
Start with an explicit output shape
This original two-session fixture is small enough to compare by hand:
<section id="sessions">
<article class="session" data-id="007">
<h2>Repair clinic</h2>
<time datetime="2026-11-04">November 4</time>
<a href="./repair">Session details</a>
</article>
<article class="session" data-id="008">
<h2>Lighting workshop</h2>
<time datetime="2026-11-12">November 12</time>
<a href="./lighting">Session details</a>
</article>
</section>
Choose an array of records with id, title, date, and path. Keep the IDs as strings so 007 retains its leading zeros. The datetime attribute supplies the machine-readable date; the displayed month name is a different representation.
Extract the fields from the parsed document
In a browser JavaScript environment, put the fixture above into a string named html, then run:
const doc = new DOMParser().parseFromString(html, 'text/html');
function required(element, selector) {
const found = element.querySelector(selector);
if (!found) throw new Error(`Missing ${selector}`);
return found;
}
const sessions = [...doc.querySelectorAll('#sessions .session')].map((row) => {
const id = row.getAttribute('data-id');
const title = required(row, 'h2').textContent.trim();
const date = required(row, 'time').getAttribute('datetime');
const path = required(row, 'a').getAttribute('href');
if (!id || !title || !date || !path) {
throw new Error('A session has an empty required field');
}
return { id, title, date, path };
});
if (sessions.length === 0) throw new Error('No session records found');
const json = JSON.stringify(sessions, null, 2);
console.log(json);
The browser parses the string into a separate document. querySelectorAll selects the records, and textContent reads each heading's text. DOMParser, selector behavior, and textContent document those separate operations.
The expected output is:
[
{
"id": "007",
"title": "Repair clinic",
"date": "2026-11-04",
"path": "./repair"
},
{
"id": "008",
"title": "Lighting workshop",
"date": "2026-11-12",
"path": "./lighting"
}
]
The check stops on a missing field instead of silently inventing a date or producing an incomplete record. It is a starting extraction rule, not a date-format or destination validator. Add the checks required by the receiving system before importing real data.
Decide what each link should mean
getAttribute('href') preserves the authored relative path. That is useful when another process will resolve it against the original site's address. If the JSON needs complete URLs, resolve each path against an explicit, trusted source URL with new URL(path, sourceURL) and validate the permitted schemes and destination rules.
Avoid resolving a saved page against the location of your converter. A session link could otherwise point at the converter's domain. Record the original page address alongside the extraction so a later reviewer can reproduce that decision.
Keep extraction separate from page execution
HTML parsing is not sanitization. MDN explains that a parsed HTML document is largely inert, but resource elements can still download content, and moving its nodes into the visible document can activate unsafe behavior. Use trusted local fixtures for this exercise. Do not insert an unknown parsed page into the live document just to inspect its text.
Show JSON output as literal text in a pre using textContent, or in a read-only textarea. JSON.stringify produces JSON text; it does not make that text suitable for arbitrary HTML injection.
For a table export, the HTML table to CSV guide addresses rows and columns directly. For inspecting the resulting JSON's values and types, use the JSON inspection guide. Keep the original HTML, selected fields, and extracted record count together until the import has been checked against the source.