# How h1 and h2 HTML Tags Define Channel Group Hierarchy in Free-TV IPTV Markdown

> Understand how h1 and h2 HTML tags structure channel group hierarchy in Free-TV IPTV markdown. Learn to define countries and broadcast standards for M3U8 playlists.

- Repository: [Free TV/IPTV](https://github.com/Free-TV/IPTV)
- Tags: how-to-guide
- Published: 2026-06-17

---

**In the Free-TV/IPTV repository, `<h1>` tags define top-level channel groups (countries) and `<h2>` tags define sub-groups (broadcast standards), which [`make_playlist.py`](https://github.com/Free-TV/IPTV/blob/main/make_playlist.py) combines into hierarchical `#EXTGRP:` entries in the final M3U8 playlist.**

The Free-TV/IPTV project organizes thousands of free television channels using a lightweight Markdown catalog system. Rather than relying on complex databases or YAML front-matter, the repository leverages standard HTML heading tags within Markdown files to establish a strict two-level hierarchy that drives automated playlist generation.

## The Two-Level Hierarchy: h1 as Groups and h2 as Sub-Groups

The channel organization system relies on exactly two HTML heading levels embedded in plain-text Markdown. According to the repository's README, the `<h1>` tag serves as the **group title**—typically representing a country or major geographic region that becomes a discrete group in the playlist.

When [`make_playlist.py`](https://github.com/Free-TV/IPTV/blob/main/make_playlist.py) processes a Markdown file from the `lists/` directory, it extracts the first `<h1>` element to create the base group name. The `<h2>` tag defines **sub-groups** within that main category, such as broadcast standards (ATSC, DVB-S) or service types (Pluto). When the parser encounters an `<h2>` line, it generates a combined `#EXTGRP:` entry that joins the h1 and h2 values with an en-dash separator.

For example, [`lists/usa.md`](https://github.com/Free-TV/IPTV/blob/main/lists/usa.md) begins with `<h1>USA</h1>` on line 1. Lines 3, 14, 28, and 35 contain `<h2>` tags defining sub-groups like "ATSC", "DVB-S", and "Pluto". This structure produces playlist entries such as:

```

#EXTGRP:USA – ATSC
#EXTGRP:USA – DVB-S
#EXTGRP:USA – Pluto

```

## How make_playlist.py Parses the Markdown Headings

The conversion logic in [`make_playlist.py`](https://github.com/Free-TV/IPTV/blob/main/make_playlist.py) uses regular expressions to extract hierarchy information without requiring a full Markdown parser. The script reads each `.md` file in the `lists/` directory and applies pattern matching to identify the heading structure.

The extraction follows this precise workflow:

1. Capture the top-level group using `re.search(r'<h1>(.*?)</h1>', text).group(1)`
2. Iterate through all sub-groups using `re.finditer(r'<h2>(.*?)</h2>', text)`
3. Combine the values as `f'{group} – {subgroup}'` for each `#EXTGRP:` entry

This regex-based approach allows the generator to process plain-text Markdown files efficiently while ignoring the table-based channel listings that follow the headings.

## Real-World Implementation in lists/usa.md

The [`lists/usa.md`](https://github.com/Free-TV/IPTV/blob/main/lists/usa.md) file demonstrates this hierarchy in production usage. The file structure follows a strict pattern where the `<h1>` establishes the country context and subsequent `<h2>` tags partition channels by technical standard or source.

As implemented in Free-TV/IPTV, the parser treats each `<h2>` boundary as a new sub-group container. Channels listed in the Markdown tables beneath an `<h2>` heading inherit that sub-group classification until the next `<h2>` tag appears. This creates a clean separation between different broadcast types (terrestrial ATSC versus satellite DVB-S) while maintaining the top-level "USA" grouping.

## Creating a New Channel List with Proper Heading Structure

To add a new country catalog, create a Markdown file in the `lists/` directory following this exact pattern:

```markdown
<h1>Canada</h1>

<h2>ATSC</h2>
| # | Channel | Link | Logo | EPG id |

|---|---------|------|------|--------|
| 1 | CBC | [>](https://example.com/cbc.m3u8) | <img src="..." /> | CBC.ca |

<h2>DVB-S</h2>
| # | Channel | Link | Logo | EPG id |

|---|---------|------|------|--------|
| 1 | Global TV | [>](https://example.com/global.m3u8) | <img src="..." /> | Global.ca |

```

The `<h1>` tag creates the "Canada" group, while each `<h2>` tag generates a distinct sub-group (Canada – ATSC, Canada – DVB-S). No additional metadata, YAML front-matter, or indentation is required to establish this relationship.

## Summary

- `<h1>` tags in Markdown files define top-level channel groups (typically countries) that serve as the base name in [`make_playlist.py`](https://github.com/Free-TV/IPTV/blob/main/make_playlist.py)
- `<h2>` tags create sub-groups that combine with the h1 value using the format "Group – SubGroup" for `#EXTGRP:` entries
- The parser extracts headings using regex patterns `r'<h1>(.*?)</h1>'` and `r'<h2>(.*?)</h2>'` rather than a Markdown parser
- Channel lists reside in the `lists/` directory and require no special configuration beyond these two HTML heading levels

## Frequently Asked Questions

### Can I use standard Markdown `#` headings instead of HTML `<h1>` tags?

No. The [`make_playlist.py`](https://github.com/Free-TV/IPTV/blob/main/make_playlist.py) script specifically searches for HTML `<h1>` and `<h2>` tags using regex patterns. Standard Markdown ATX headings (`# Heading`) will not be recognized as group delimiters during playlist generation, and the channels will not be properly categorized in the output.

### What happens if a Markdown file lacks an `<h1>` tag?

The parsing logic expects an `<h1>` element to establish the base group name. Without it, the script cannot properly prefix sub-groups, potentially causing incomplete or malformed `#EXTGRP:` entries in the generated M3U8 playlist. Every channel list file must contain exactly one `<h1>` tag at the beginning.

### Is there a limit to how many `<h2>` sub-groups can exist under one `<h1>`?

There is no enforced limit in the parsing logic. The `re.finditer()` function processes all `<h2>` occurrences sequentially, allowing unlimited sub-groups (ATSC, DVB-S, Cable, Pluto, etc.) beneath a single country heading. Each sub-group simply generates a new combined entry in the playlist.

### Does the hierarchy support levels deeper than h2 (such as h3)?

No. The current implementation in [`make_playlist.py`](https://github.com/Free-TV/IPTV/blob/main/make_playlist.py) only recognizes `<h1>` and `<h2>` tags for playlist organization. Any `<h3>` or deeper headings would be treated as standard content and ignored during group extraction, making them unsuitable for defining additional hierarchy levels.