Neu im Forum:
Strukturierte Daten für Portfolio/Referenzen
https://t3forum.net/d/1100-strukturierte-daten-fuer-portfolioreferenzen
Adding AI Usage Metadata to JSON-LD Structured Data
https://rmendes.net/articles/2026/03/03/adding-ai-usage-metadata-to
Does ordering of keys impact interpretation?
Maximally Semantic Structure for a Blog Post
https://shkspr.mobi/blog/2026/01/maximally-semantic-structure-for-a-blog-post/Yes, I know the cliché that bloggers are always blogging about blogging!
I like semantics. It tickles that part of my delicious meaty brain that longs for structure. Semantics are good for computers and humans. Computers can easily understand the structure of the data, humans can use tools like screen-readers to extract the data they're interested in.
In HTML, there are three main ways to impose semantics - elements, attributes, and hierarchical microdata.
Elements are easy to understand. Rather than using a generic element like <div> you can use something like <nav> to show an element's contents are for navigation. Or <address> to show that the contents are an address. Or <article><section> to show that the section is part of a parent article.
Attributes are also common. You can use relational attributes to show how a link relates to the page it is on. For example <a rel=author href=https://example.com> shows that the link is to the author of the current page. Or, to see that a link goes to the previous page in a series <a rel=prev href=/page5>.
Finally, we enter the complex and frightening world of microdata.
Using the Schema.org vocabulary it's possible to add semantic metadata within an HTML element. For example, <body itemtype=https://schema.org/Blog itemscope> says that the body of this page is a Blog. Or, to say how many words a piece has, <span itemprop=wordCount content=1100>1,100 words</span>.
There are many properties you can use. Here's the outline structure of a single blog post with a code sample, a footnote, and a comment. You can check its structured data and verify that it is conformant HTML.
Feel free to reuse.
<!doctype html>
<html lang=en-gb>
<head><title>My Blog</title></head>
<body itemtype=https://schema.org/Blog itemscope>
<header itemprop=headline>
<a rel=home href=https://example.com>My Blog</a>
</header>
<main itemtype=https://schema.org/BlogPosting itemprop=blogPost itemscope>
<article>
<header>
<time itemprop=https://schema.org/datePublished datetime=2025-12-01T12:34:39+01:00>
1st January, 2025
</time>
<h1 itemprop=headline>
<a rel=bookmark href=https://example.com/page>Post Title</a>
</h1>
<span itemtype=https://schema.org/Person itemprop=author itemscope>
<a itemprop=url href=https://example.org/>
By <span itemprop=name>Author Name</span>
</a>
<img itemprop=image src=/photo.jpg alt>
</span>
<p>
<a itemprop=keywords content=HTML rel=tag href=/tag/html/>HTML</a>
<a itemprop=keywords content=semantics rel=tag href=/tag/semantics/>semantics</a>
<a itemprop=commentCount content=6 href=#comments>6 comments</a>
<span itemprop=wordCount content=1100>1,100 words</span>
<span itemtype=https://schema.org/InteractionCounter itemprop=interactionStatistic itemscope>
<meta content=https://schema.org/ReadAction itemprop=interactionType>
<span itemprop=userInteractionCount content=5150>
Viewed ~5,150 times
</span>
</span>
</p>
</header>
<div itemprop=articleBody>
<img itemprop=image src=/hero.png alt>
<p>Text of the post.</p>
<p>Text with a footnote<sup id=fnref><a role=doc-noteref href=#fn>0</a></sup>.</p>
<pre itemtype=https://schema.org/SoftwareSourceCode itemscope translate=no>
<span itemprop=programmingLanguage>PHP</span>
<code itemprop=text>&lt;?php echo $postID ?&gt;</code>
</pre>
<section role=doc-endnotes>
<h2>Footnotes</h2>
<ol>
<li id=fn>
<p>Footnote text. <a role=doc-backlink href=#fnref>↩︎</a></p>
</li>
</ol>
</section>
</div>
</article>
<section id=comments>
<h2>Comments</h2>
<article itemtype=https://schema.org/Comment itemscope id="comment-123465">
<time itemprop=dateCreated datetime=2025-09-11T13:24:54+01:00>
<a itemprop=url href=#comment-123465>2025-09-11 13:24</a>
</time>
<div itemtype=https://schema.org/Person itemprop=author itemscope>
<img itemprop=image src="/avatar.jpg" alt>
<h3>
<span itemprop=name>Alice</span> says:
</h3>
</div>
<div itemprop=text>
<p>Comment text</p>
</div>
</article>
</section>
</main>
</body>
</html>
This blog post is entitled "maximally" but, of course, there is lots more that you can add if you really want to.
Remember, none of this is necessary. Computers and humans are pretty good at extracting meaning from unstructured text. But making things easier for others is always time well spent.
#blogging #HTML #schemaOrg #semanticWeb
Yes, I know the cliché that bloggers are always blogging about blogging! I like semantics. It tickles that part of my delicious meaty brain that longs for structure. Semantics are good for computers and humans. Computers can easily understand the structure of the data, humans can use tools like screen-readers to extract the data they're interested in. In HTML, there are three main ways to …
Fixing "Date/time not in ISO 8601 format" in Google Search Console
https://shkspr.mobi/blog/2025/12/fixing-date-time-not-in-iso-8601-format-in-google-search-console/I like using microdata within my HTML to provide semantic metadata. One of my pages had this scrap of code on it:
<time
itemprop="datePublished"
itemscope
datetime="2025-06-09T11:27:06+01:00">9 June 2025 11:27</time>
The Google Search Console was throwing this error:
I was fairly sure that was a valid ISO 8601 string. It certainly matched the description in the Google documentation. Nevertheless, I fiddled with a few different formats, but all failed.
On the advice of Barry Hunter, I tried changing the datetime attribute to content. That also didn't work.
Then I looked closely at the code.
The issue is the itemscope. Removing that allowed the code to pass validation. But why?
Here's what the Schema.org documentation says:
By adding itemscope, you are specifying that the HTML contained in the block is about a particular item.
The HTML specification gives this example:
<div itemscope>
<img itemprop="image" src="google-logo.png" alt="Google">
</div>
Here, the image property is the value of the element. In this case google-logo.png. So what's the problem with the time example?
Well, <image> is a void element. It doesn't have any HTML content - so the metadata is taken from the src attribute.
But <time> is not a void element. It does contain HTML. So something like this would be valid:
<time
itemprop="datePublished"
itemscope
>2025-06-09T11:27:06+01:00</time>
The text contained by the element is a valid ISO8601 string.
My choice was either to present the ISO8601 string to anyone viewing the page, or simply to remove the itemscope. So I chose the latter.

I like using microdata within my HTML to provide semantic metadata. One of my pages had this scrap of code on it: HTML<time itemprop="datePublished" itemscope datetime="2025-06-09T11:27:06+01:00">9 June 2025 11:27</time> The Google Search Console was throwing this error: I was fairly sure that was a valid ISO 8601 string. It certainly matched the description in the Google…
AI-агенты для SEO: как автоматизировать 98% рутины и не потерять качество
Четыре месяца назад я сидел в офисе клиента в Минске. Владелец интернет-магазина спортивного питания смотрел на график Analytics. Линия трафика ползла вниз. Медленно. Но неумолимо. «Мы делаем всё правильно», — сказал он. И был прав. Контент. Техническая оптимизация. Ссылочная масса. Команда из трёх SEO-специалистов работала на пределе. Но конкурент из Москвы обгонял их каждую неделю. По всем фронтам. Я открыл сайт конкурента. Замер. За последние три месяца они опубликовали 90 новых статей. Детальные гайды по спортпиту. Сравнения. Обзоры. Внутренняя перелинковка выстроена хирургически точно. Schema.org разметка на каждой странице. Технические параметры — как у enterprise-проекта. Позвонил знакомому, который работает в той компании. Спросил прямо: «У вас что, команда из двадцати человек?» Пауза. Смех. «Один SEO-специалист. Плюс AI-агенты. Автоматизировали 98% процессов.» Вот тогда я понял. Правила изменились. И большинство об этом ещё не знает.
https://habr.com/ru/articles/955108/
#AIагенты_для_SEO #автоматизация_SEO #ai_overviews #schemaorg #eeat #кластеризация_запросов #внутренние_ссылки #линкбилдинг #keyword_research #техническое_SEO
Class Warfare! Can I eliminate CSS classes from my HTML?
https://shkspr.mobi/blog/2025/09/class-warfare-can-i-eliminate-css-classes-from-my-html/
I recently read a brilliantly provocative blog post called "This website has no class". In it, Adam Stoddard makes the case that you might not need CSS classes on a modern website:
I think constraints lead to interesting, creative solutions […]. Instead of relying on built in elements a bit more, I decided to banish classes from my website completely.
Long time readers will know that I'm a big fan of using semantic HTML where possible. If you peek beneath the curtain of this website you'll only see a handful of <div> elements (mostly because WordPress hardcodes them) - all the other blocks are fully semantic. Regrettably, there are rather too many <span> elements for my liking - normally for accessibility or for supplementing the metadata.
Overall, my CSS contained about 134 rules which selected based on class. Is that a lot? It feels like a lot.
On the one hand, classes are an easy way of splitting and grouping elements. Some <img>s should be displayed one way, the rest another. There's no semantic way to say "This is a hero image and should take up the full width, but this is an icon and should float discretely to the right."
But, on the other hand, why do we need classes? Keith Cirkel's excellent post "CSS Classes considered harmful" goes through their history and brings together some proposed solutions for replacing them. I think his idea of using data- attributes is a neat hack - but ultimately isn't much different from using classes. It's still a scrap of metadata to be tied into a style-sheet.
Classes are great for when you reuse something. I have multiple <section> elements but most don't share anything in common with the others. So they probably oughtn't have classes.
Removing classes has some advantages. It makes the HTML fractionally smaller, sure, but it also forces the author to think about the logical structure of their page and the semantics behind it.
Looking through my HTML, lots of classes exist because of laziness. If I want to position all the <time> elements which are within a comment, I don't need to write <time class="whatever"> and to pair it with .whatever { … }. Instead, I can use modern CSS selectors and say #comments time { … }.
But this leads me on to another existential question.
Are IDs necessary in modern HTML?
Mayyyyybe? I only have one <main> element, so an ID on there is unnecessary. <input> elements need IDs in order to be properly targetted by <label>s - but the label can wrap around the input. I have multiple <aside> elements because there's no semantic <widget> element, so they need unique IDs.
In theory, as suggested by Adam above, I could use an autonomous custom element like <my-widget> - but that has none of the semantics and, frankly, feels like a bit of a cheat.
Trimming the fat
Any day where I can delete some code is a good day. This was an excellent exercise in going through (years) of HTML and CSS to see what cruft had built up.
The first CSS rule I changed was, as mentioned above:
CSStime.commentmetadata { float: right;}Which became:
CSS#comments time { float: right;}Classless and slightly more brief. Is it more readable? Having the fact it was about the metadata in a class could have been slightly useful - but if I thought it would be confusing, I could stick a /* comment */ in there.
Next, I found <nav class="navigation posts-navigation"> - what a tautology! I have multiple <nav> elements, it is true. But none of them have the same style. So this swiftly became <nav id="posts-navigation"> with an accompanying CSS rewrite.
My theme switcher had a bunch of <label class=button>s. They were all within a container with a unique ID, so could they be changed? Yes. But seeing the class name in the HTML is a good reminder to the author of how they are meant to display. Does that co-mingle content and presentation too much?
Some of the WordPress default classes are ridiculous. The body_class() function injected this into every <body>
"wp-singular post-template-default single single-post postid-62959 single-format-standard wp-theme-edent-wordpress-theme"
Most of that is redundant - what's the difference between single and single-post? For my purposes, nothing! So they were all yeeted into the sun.
Rather than targetting IDs or classes, I targetted the presence or absence of Schema.org microdata.
For example:
CSSmain[itemprop="blogPost"] { … }main:not([itemprop="blogPost"]) { … }This can go to the extreme. I have lots of comments, each one has an author, the author's details are wrapped in <div class="authordetails">…</div>
That can be replaced with:
CSS/* Comment Author */li[itemtype="https://schema.org/Comment"] > article > div[itemprop="https://schema.org/author"] { margin-bottom: 0;}Is that sensible? It is more semantic, but feels a bit brittle.
Parent selector are also now a thing. If I want a paragraph to have centred text but only when there's a submit button inside it:
CSSp:has(input#submit) { text-align: center;}Again, am I sure that my button will always be inside a paragraph?
Similarly, sibling selectors are sometimes superior - but they do suppose that your layout never changes.
What remains?
There are some bits of this site which are reusable and do need classes. The code-highlighting you see above requires text to be wrapped in spans with specific classes.
Image alignment was also heavily class based.
There are some accessibility things which are either hidden or exposed using classes.
A bunch of WordPress defaults use classes and, even if they are redundant, it's hard to exorcise them.
As much as I would have liked to get rid of all my IDs, many needed to stay for linking as well as CSS targetting.
All told, the changes I made were:
I have around 250 CSS rules, so now the majority target semantics rather than classes or IDs.
Is this really necessary?
No, of course not. This is an exercise in minimalism, creativity, and constraint. Feel free to litter your HTML with whatever attributes you want!
As I went through, it increasingly became apparent that I was fitting my CSS to my HTML's logical structure rather than to its conceptual structure.
Previously, my comments were targetted with a class. Now they have the slightly more tangled targetting of "divs with this schema attribute whose parent is an article and whose grandparent has this ID".
It is a delightful meditative exercise to go through your code and deeply consider whether something is unique, reusable, or obsolete.
I recently read a brilliantly provocative blog post called "This website has no class". In it, Adam Stoddard makes the case that you might not need CSS classes on a modern website: I think constraints lead to interesting, creative solutions […]. Instead of relying on built in elements a bit more, I decided to banish classes from my website completely. Long time readers will know that I'm a big f…
Adding Semantic Reviews / Rich Snippets to your WordPress Site
https://shkspr.mobi/blog/2020/07/adding-semantic-reviews-rich-snippets-to-your-wordpress-site/
This is a real "scratch my own itch" post. I want to add Schema.org semantic metadata to the book reviews I write on my blog. This will enable "rich snippets" in search engines.
There are loads of WordPress plugins which do this. But where's the fun in that?! So here's how I quickly built it into my open source blog theme.
Screen options
First, let's add some screen options to the WordPress editor screen.
This is what it will look like when done:
This is how to add a custom metabox to the editor screen:
// Place this in functions.php// Display the boxfunction edent_add_review_custom_box(){ $screens = ['post']; foreach ($screens as $screen) { add_meta_box( 'edent_review_box_id', // Unique ID 'Book Review Metadata', // Box title 'edent_review_box_html',// Content callback, must be of type callable $screen // Post type ); }}add_action('add_meta_boxes', 'edent_add_review_custom_box');The contents of the box are bog standard HTML
// Place this in functions.php// HTML for the boxfunction edent_review_box_html($post){ $review_data = get_post_meta(get_the_ID(), "_edent_book_review_meta_key", true); echo "<table>"; $checked = ""; if ($review_data["review"] == "true") { $checked = "checked"; } echo "<tr><td><label for='edent_book_review'>Embed Book Review:</label></td><td><input type=checkbox id=edent_book_review name=edent_book_review[review] value=true {$checked}></tr>"; echo "<tr><td><label for='edent_rating'>Rating:</label></td><td><input type=range id=edent_rating name=edent_book_review[rating] min=0 max=5 step=0.5 value='". esc_html($review_data["rating"]) ."'></tr>"; echo "<tr><td><label for=edent_isbn >ISBN:</label></td><td><input name=edent_book_review[isbn] id=edent_isbn type=text value='" . esc_html($review_data["isbn"]) . "' autocomplete=off></tr>"; echo "</table>";}Done! We now have a box for metadata. That data will be POSTed every time the blogpost is saved. But where do the data go?
Saving data
This function is added every time the blogpost is saved. If the checkbox has been ticked, the metadata are saved to the database. If the checkbox is unticked, the metadata are deleted.
// Place this in functions.php// Save the boxfunction edent_review_save_postdata($post_id){ if (array_key_exists('edent_book_review', $_POST)) { if ($_POST['edent_book_review']["review"] == "true") { update_post_meta( $post_id, '_edent_book_review_meta_key', $_POST['edent_book_review'] ); } else { delete_post_meta( $post_id, '_edent_book_review_meta_key' ); } }}add_action('save_post', 'edent_review_save_postdata');Nice! But how do we get the data back out again?
Retrieving the data
We can use the get_post_meta() function to get all the metadata associated with a blog entry. We can then turn it into a Schema.org structured metadata entry.
function edent_book_review_display($post_id){ // https://developer.wordpress.org/reference/functions/the_meta/ $review_data = get_post_meta($post_id, "_edent_book_review_meta_key", true); if ($review_data["review"] == "true") { $blog_author_data = get_the_author_meta(); $schema_review = array ( '@context' => 'https://schema.org', '@type' => 'Review', 'author' => array ( '@type' => 'Person', 'name' => get_the_author_meta("user_firstname") . " " . get_the_author_meta("user_lastname"), 'sameAs' => array ( 0 => get_the_author_meta("user_url"), ), ), 'url' => get_permalink(), 'datePublished' => get_the_date('c'), 'publisher' => array ( '@type' => 'Organization', 'name' => get_bloginfo("name"), 'sameAs' => get_bloginfo("url"), ), 'description' => mb_substr(get_the_excerpt(), 0, 198), 'inLanguage' => get_bloginfo("language"), 'itemReviewed' => array ( '@type' => 'Book', 'name' => $review_data["title"], 'isbn' => $review_data["isbn"], 'sameAs' => $review_data["book_url"], 'author' => array ( '@type' => 'Person', 'name' => $review_data["author"], 'sameAs' => $review_data["author_url"], ), 'datePublished' => $review_data["book_date"], ), 'reviewRating' => array ( '@type' => 'Rating', 'worstRating' => 0, 'bestRating' => 5, 'ratingValue' => $review_data["rating"], ), 'thumbnailUrl' => get_the_post_thumbnail_url(), ); echo '<script type="application/ld+json">' . json_encode($schema_review) . '</script>'; echo "<div class='edent-review' style='clear:both;'>"; if (isset($review_data["rating"])) { echo "<span class='edent-rating-stars' style='font-size:2em;color:yellow;background-color:#13131380;'>"; $full = floor($review_data["rating"]); $half = 0; if ($review_data["rating"] - $full == 0.5) { $half = 1; } $empty = 5 - $half - $full; for ($i=0; $i < $full ; $i++) { echo "★"; } if ($half == 1) { echo "⯪"; } for ($i=0; $i < $empty ; $i++) { echo "☆"; } echo "</span>"; } echo "<ul>"; if ($review_data["amazon_url"] != "") { echo "<li><a href='{$review_data["amazon_url"]}'>Buy it on Amazon</a></li>"; } if ($review_data["author_url"] != "") { echo "<li><a href='{$review_data["author_url"]}'>Author's homepage</a></li>"; } if ($review_data["book_url"] != "") { echo "<li><a href='{$review_data["book_url"]}'>Publisher's details</a></li>"; } echo "</ul>"; } echo "</div>";}In index.php, after the_content(); add:
edent_book_review_display(get_the_ID());Then, on the website, it will look something like this:
Note the use of the Unicode Half Star for the ratings.
The source code of the site shows the output of the JSON LD:
When run through a Structured Data Testing Tool, it shows as a valid review:
And this means, when search engines access your blog, they will display rich snippets based on the semantic metadata.
You can see the final blog post to see how it works.
ToDo
My code is horrible and hasn't been tested, validated, or sanitised. It's only for my own blog, and I'm unlikely to hack myself, but that needs fixing.
I want to add review metadata for movies, games, and gadgets. That will either require multiple boxes, or a clever way to only show the necessary fields.
This is a real "scratch my own itch" post. I want to add Schema.org semantic metadata to the book reviews I write on my blog. This will enable "rich snippets" in search engines. There are loads of WordPress plugins which do this. But where's the fun in that?! So here's how I quickly built it into my open source blog theme. Screen options First, let's add some screen options to the WordPress…