Gemini Structured Output With Zod in Next.js
Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.
HTML semantic tags are elements whose names describe what their content is, not how it looks. <header>, <nav>, <main>, <article>, <section>, <aside> and <footer> tell browsers, screen readers and search engines what each part of the page is for, while a <div> tells them nothing. In this post I explain each semantic tag with examples, show how I structure a real page, and list the mistakes I made when I first started using them.
When I built my first websites, every box was a <div> with a class name. The pages looked fine, but the HTML didn't describe anything. Switching to semantic tags didn't change how my pages looked at all. It changed how well they worked for everyone who can't just look at the screen.
Here's a typical "div soup" layout, the kind I used to write:
<div class="header">
<div class="logo">My Blog</div>
<div class="menu">
<div class="link"><a href="/">Home</a></div>
<div class="link"><a href="/blog">Blog</a></div>
</div>
</div>
<div class="content">
<div class="post">
<div class="title">My first post</div>
<div class="text">Hello!</div>
</div>
</div>
<div class="footer">© 2026</div>
It's valid HTML, but a screen reader has no idea which part is the navigation and which part is the main content. The class names header and menu mean something to me, not to the browser.
Now the same page with semantic tags:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>My Blog</title>
</head>
<body>
<header>
<a href="/">My Blog</a>
<nav aria-label="Main">
<ul>
<li><a href="/">Home</a></li>
<li><a href="/blog">Blog</a></li>
<li><a href="/projects">Projects</a></li>
</ul>
</nav>
</header>
<main>
<article>
<h1>My first post</h1>
<p>Hello!</p>
</article>
<aside aria-label="About the author">
<h2>About me</h2>
<p>Student web developer from Bangladesh.</p>
</aside>
</main>
<footer>
<p>© 2026 My Blog</p>
</footer>
</body>
</html>
Same content, but now the structure is built into the HTML. Screen reader users can jump straight to the navigation or the main content, because these elements map to landmarks. Search engines get clearer signals about which text is the actual article. And when I come back to the code a month later, I can see the layout at a glance.
Introductory content, usually a logo, site title, navigation or a search form. A page can have more than one: one at the top of the page and one inside each <article> for its title and date. When a <header> isn't inside an article, section or similar element, it becomes the page's banner landmark.
A block of major navigation links, like the main menu, a table of contents or pagination. Not every group of links needs a <nav>. A couple of links in the footer are usually fine without one. If you have more than one <nav>, give each an aria-label (like "Main" or "Footer") so screen reader users can tell them apart.
The main content of the page, the part that's unique to it. There should be only one visible <main> per page, and it shouldn't be inside <header>, <footer>, <nav>, <article> or <aside>. Repeated things like the site header and footer stay outside it.
A self-contained piece of content that would still make sense on its own, such as a blog post, a news story, a product card or a comment. A good test: could you share it or put it in an RSS feed by itself? If yes, it's probably an article.
A thematic group of content, normally with its own heading. Think chapters of a post or the "Features" and "Pricing" parts of a landing page. If you only need a wrapper for styling or layout, use a <div>, and do the layout itself with Flexbox or Grid. A <section> without a heading is often a sign you wanted a div.
Content that's related to the content around it but not part of its main flow: a sidebar, an author box, a "related posts" list or a pull quote.
Information about its section or page: copyright, author, related links, contact details. Like <header>, it can be used for the whole page or inside an article.
Here's how I'd mark up a single blog post. It uses an article-level header and footer, sections for the main parts, <time> for the date and <figure> for an image with a caption:
<article>
<header>
<h2>CSS Flexbox vs Grid</h2>
<p>Published <time datetime="2026-10-02">2 October 2026</time></p>
</header>
<section>
<h3>When to use Flexbox</h3>
<p>Flexbox lays items out in one direction.</p>
</section>
<section>
<h3>When to use Grid</h3>
<p>Grid handles rows and columns together.</p>
</section>
<figure>
<img src="layout-diagram.png" alt="Flexbox row next to a two-dimensional grid" width="800" height="400">
<figcaption>Flexbox works in one dimension, Grid in two.</figcaption>
</figure>
<footer>
<p>Tags: <a href="/tag/css">CSS</a></p>
</footer>
</article>
<time datetime="..."> gives machines an exact date in a standard format, while the visible text can be written however you like.<figure> with <figcaption> connects an image, chart or code sample to its caption. The alt text still matters: the caption is for everyone, the alt text replaces the image for people who can't see it.<section> resets heading levels (the "document outline algorithm") was never implemented by browsers and has been removed from the HTML spec. Use <h1> to <h6> in a logical order yourself.Semantics isn't only about page layout. Inline elements carry meaning too:
<p><strong>Warning:</strong> this deletes every note.</p>
<p>I <em>really</em> mean every note.</p>
<p>Press <kbd>Ctrl</kbd> + <kbd>S</kbd> to save.</p>
<p>Run <code>npm install</code> first.</p>
<p>The exam is on <time datetime="2026-11-15">15 November</time>.</p>
<p><mark>Highlighted</mark> text and an <abbr title="HyperText Markup Language">HTML</abbr> abbreviation.</p>
<address>Contact: <a href="mailto:hello@example.com">hello@example.com</a></address>
<strong> means strong importance, and <em> means stress emphasis that changes how a sentence reads. <b> and <i> still exist, for text that's styled differently without being more important, like a term or a ship name.<kbd> is for keyboard input, <code> for code, <mark> for highlighted text and <abbr> for abbreviations.<address> is for contact information for the nearest article or the page. It's not meant for every postal address that appears in your text.Two more I use often are <details> and <summary>, which give you an accessible open/close widget without any JavaScript:
<details>
<summary>What is a semantic tag?</summary>
<p>A tag whose name describes the meaning of its content.</p>
</details>
And there's a newer one, <search>, for wrapping a search form. It gives the form a search landmark without needing role="search", and it's supported in current versions of the major browsers:
<search>
<form action="/search">
<label for="q">Search posts</label>
<input id="q" name="q" type="search">
<button type="submit">Search</button>
</form>
</search>
This is the most harmful one, and I've done it:
<!-- Avoid: not focusable, no keyboard support, not announced as a button -->
<div class="btn" onclick="saveNote()">Save</div>
<!-- Better: works with Tab, Enter and Space out of the box -->
<button type="button" onclick="saveNote()">Save</button>
A <div> with a click handler can't be reached with the Tab key, doesn't respond to Enter or Space, and isn't announced as a button. A real <button> does all of that for free. Use <a href> for things that go somewhere and <button> for things that do something.
Replacing every div with <section> doesn't make a page more semantic. If there's no heading and no real theme, it should stay a div.
Choosing <h4> because it looks the right size breaks the heading structure that screen reader users navigate by. Pick the level that matches the structure, then change the size with CSS.
Keep one visible <main> per page, as a direct part of the body layout, not nested inside other landmarks.
Most of these elements look exactly like a div by default. Semantics is about meaning. You still write the CSS.
They help search engines understand your page structure and find the main content, and they make the page more accessible, which is good practice anyway. They're not a magic ranking boost on their own. Useful content and a clear heading structure matter more.
An <article> makes sense on its own, like a full blog post. A <section> is one part of something bigger, like one chapter of that post. Articles can contain sections, and a section can contain several articles, like a list of post cards.
No. <div> is the right choice when you need a container for styling or layout and no semantic element fits. The goal is to use meaningful tags where they exist, not to ban divs.
Yes. You can have a page-level header and footer, plus a header and footer inside each article or section.
Semantic HTML means picking the element that describes your content: <header> and <footer> for intro and closing info, <nav> for major links, one <main> for the unique content, <article> for things that stand alone, <section> for themed parts with headings, and <aside> for related extras. Use real buttons and real headings, and keep <div> for pure layout. For forms, my guide to HTML form input types and validation covers the right elements and attributes.
MDN's HTML elements reference is where I check what each element is for. If you want to see what I build with it, my projects page has the list.
// note
This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.
If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.
Describe the JSON once in Zod, send z.toJSONSchema() to Gemini as responseJsonSchema, then validate the reply with safeParse and retry once with the errors.
Step by step: a Gemini key from Google AI Studio in .env.local, a server-only helper, a Next.js route handler, Vercel environment variables and a leak test.
A small offline habit tracker in plain HTML and JavaScript: habits saved as JSON in localStorage, streaks from local dates, a 7-day row and a JSON backup.