Most structured data guides begin and end with Article, Product, and BreadcrumbList. For developer-focused sites — blogs covering tools, libraries, and technical how-tos — two schema types deserve far more attention: TechArticle and SoftwareApplication. Both are valid Schema.org types that Google's Rich Results Test recognises, both are chronically underused, and both map naturally onto the kind of content a developer blog publishes. This guide covers what each type does, how to implement them correctly in JSON-LD, and how to integrate the output into Drupal twig templates.
Why TechArticle and SoftwareApplication?
TechArticle is a sub-type of Article designed for technical documentation and how-to content. It adds two properties that generic articles lack: dependencies (e.g. "PHP 8.3, Drupal 10") and proficiencyLevel (e.g. "Expert"). These signal to search engines the nature of the content without it needing to be inferred from body text.
SoftwareApplication is the correct type for pages describing a tool, module, plugin, or command-line utility. It supports properties like operatingSystem, applicationCategory, softwareVersion, and nested AggregateRating or Offer blocks. If your site reviews or documents Drupal modules, Composer packages, or CLI tools, SoftwareApplication is a better fit than Article.
Using the correct sub-type rather than the generic parent type gives search engines more precise signals. It also future-proofs the markup: Google periodically expands rich result support to additional schema types, and having the correct type in place means you benefit automatically.
TechArticle: Full JSON-LD Implementation
A well-formed TechArticle block for a tutorial or how-to post looks like this:
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "How to Configure Redis as a Drupal Cache Backend",
"description": "A step-by-step guide to replacing Drupal's database cache with Redis using the redis module and PhpRedis extension.",
"image": "https://linkhub.dk/sites/default/files/articles/redis-drupal-cache.jpg",
"datePublished": "2026-03-15",
"dateModified": "2026-04-01",
"author": {
"@type": "Person",
"name": "Lars Nielsen",
"url": "https://linkhub.dk/about"
},
"publisher": {
"@type": "Organization",
"name": "linkhub.dk",
"logo": {
"@type": "ImageObject",
"url": "https://linkhub.dk/logo.png"
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://linkhub.dk/articles/drupal-redis-cache"
},
"dependencies": "Drupal 10, PHP 8.2+, PhpRedis extension, drupal/redis ^2.0",
"proficiencyLevel": "Expert",
"articleSection": "DevOps",
"inLanguage": "en-GB",
"keywords": "Drupal, Redis, cache backend, PhpRedis, performance"
}
The key additions over a plain Article are dependencies and proficiencyLevel. The Schema.org specification does not constrain the values of either — use human-readable strings that accurately describe your content's requirements and intended audience.
SoftwareApplication: Full JSON-LD Implementation
A module review or tool documentation page warrants SoftwareApplication. Here is a complete example for a Drupal module overview page:
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Drupal Redis Module",
"description": "Integrates Redis as a cache backend for Drupal 10 and 11, supporting PhpRedis and Predis clients with support for cache tags and permanent storage.",
"url": "https://www.drupal.org/project/redis",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Linux, macOS, Windows",
"softwareVersion": "2.0.0",
"softwareRequirements": "PHP 8.1+, Redis 6+, drupal/core ^10 || ^11",
"offers": {
"@type": "Offer",
"price": "0",
"priceCurrency": "USD"
},
"aggregateRating": {
"@type": "AggregateRating",
"ratingValue": "4.5",
"ratingCount": "128",
"bestRating": "5",
"worstRating": "1"
},
"author": {
"@type": "Organization",
"name": "Drupal Community",
"url": "https://www.drupal.org"
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://linkhub.dk/tools/drupal-redis-module"
},
"inLanguage": "en-GB"
}
The Offer block is required if you want Google to potentially surface pricing information. For free, open-source tools, set price to "0". The aggregateRating block unlocks star ratings in search results, but only include it if you have genuine user ratings — Google will penalise fabricated scores.
Combining Types on a Single Page
A common pattern on developer blogs is a page that is both a technical article (it explains how to use a tool) and describes a software application. You can include multiple JSON-LD blocks on a single page, each in its own <script> tag:
<!-- Block 1: the article itself -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": "Getting Started with Drush: The Drupal CLI",
"dependencies": "Drupal 10, Composer, PHP 8.2+",
"proficiencyLevel": "Beginner",
"datePublished": "2026-03-01",
"author": { "@type": "Person", "name": "Lars Nielsen" },
"mainEntityOfPage": {
"@type": "WebPage",
"@id": "https://linkhub.dk/articles/drush-getting-started"
}
}
</script>
<!-- Block 2: the tool being reviewed -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "Drush",
"applicationCategory": "DeveloperApplication",
"operatingSystem": "Linux, macOS",
"softwareVersion": "13.x",
"offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" },
"url": "https://www.drush.org"
}
</script>
This approach is explicit and easy to validate. Avoid using @graph arrays unless you are confident in the syntax — errors in a @graph block can silently invalidate all nodes within it.
Implementing in Drupal Twig Templates
The cleanest approach in Drupal is to output JSON-LD from a node twig template, using fields mapped to schema properties. Assuming you have a content type called tech_article with fields such as field_dependencies and field_proficiency_level, the template node--tech-article--full.html.twig can output the schema block directly:
{#
node--tech-article--full.html.twig
Outputs TechArticle JSON-LD for all full-display tech_article nodes.
#}
{% set schema = {
"@context": "https://schema.org",
"@type": "TechArticle",
"headline": node.label,
"description": node.field_summary.value,
"datePublished": node.field_published_date.value,
"dateModified": node.changed.value | date("Y-m-d"),
"dependencies": node.field_dependencies.value,
"proficiencyLevel": node.field_proficiency_level.value,
"author": {
"@type": "Person",
"name": node.uid.entity.displayname
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": url('entity.node.canonical', {'node': node.id()}) | absolute_url
}
} %}
<script type="application/ld+json">
{{ schema | json_encode(constant('JSON_PRETTY_PRINT') b-or constant('JSON_UNESCAPED_SLASHES') b-or constant('JSON_UNESCAPED_UNICODE')) }}
</script>
{{ content }}
The json_encode filter with JSON_UNESCAPED_SLASHES is important — without it, Twig will escape forward slashes in URLs, producing invalid JSON-LD values like https:\/\/linkhub.dk\/....
Using a Drupal Module for Schema Output
If you prefer a module-based approach, the Schema.org Metatag module extends the Metatag module with structured data output. It provides UI-driven configuration for Article, SoftwareApplication, and many other types. However, it does not currently support all TechArticle-specific properties (dependencies, proficiencyLevel), so for full control a custom twig or preprocess approach is more reliable.
A lightweight custom module that attaches JSON-LD via a preprocess hook is often the cleanest production solution:
<?php
// mysite_schema.module
/**
* Implements hook_preprocess_node().
*/
function mysite_schema_preprocess_node(array &$variables): void {
$node = $variables['node'];
if ($node->bundle() !== 'tech_article' || $variables['view_mode'] !== 'full') {
return;
}
$schema = [
'@context' => 'https://schema.org',
'@type' => 'TechArticle',
'headline' => $node->label(),
'description' => $node->get('field_summary')->getString(),
'datePublished' => $node->get('field_published_date')->getString(),
'dateModified' => date('Y-m-d', $node->getChangedTime()),
'dependencies' => $node->get('field_dependencies')->getString(),
'proficiencyLevel' => $node->get('field_proficiency_level')->getString(),
'author' => [
'@type' => 'Person',
'name' => $node->getOwner()->getDisplayName(),
],
'mainEntityOfPage' => [
'@type' => 'WebPage',
'@id' => $node->toUrl('canonical', ['absolute' => TRUE])->toString(),
],
];
$variables['#attached']['html_head'][] = [
[
'#type' => 'html_tag',
'#tag' => 'script',
'#attributes' => ['type' => 'application/ld+json'],
'#value' => json_encode($schema, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE),
],
'mysite_schema_tech_article_' . $node->id(),
];
}
This attaches the JSON-LD block to the <head> element via Drupal's render pipeline, which respects caching and avoids duplicate output on pages where the same node appears multiple times.
Common Mistakes to Avoid
Using Article Instead of TechArticle
Both types are valid and both are processed by Google, but TechArticle is semantically more accurate for a step-by-step guide with technical prerequisites. Use the most specific type available.
Omitting dateModified
Google's structured data documentation explicitly states that dateModified helps it understand how fresh your content is. For technical content, freshness is a significant ranking signal — a Redis configuration guide written in 2024 that was reviewed and updated in 2026 should carry a dateModified value to reflect that.
Mismatched Content Between Schema and Page
The values in your JSON-LD must match the visible content on the page. If the headline in the schema differs from the <h1> tag, or the author in the schema does not match the byline shown to users, Google may suppress the rich result or apply a manual action for structured data misuse. Always derive schema values programmatically from the same source as the visible content.
Fabricated Ratings
Only include AggregateRating in SoftwareApplication if you have a genuine ratings system on your page. Do not hard-code a static rating value unless users can actually submit ratings. Google's guidelines are explicit on this point and violations can result in manual penalties.
Validating Your Output
Use three tools to verify your structured data before deploying:
- Google's Rich Results Test (search.google.com/test/rich-results) — paste a URL or raw HTML and see which rich result types are detected, along with any warnings or errors.
- Schema.org Validator (validator.schema.org) — validates against the full Schema.org specification, including types and properties that Google's tool does not check.
- Google Search Console — the Enhancements section shows structured data errors across your entire site, with click-through to affected URLs. Check this after deploying schema changes.
For Drupal sites, also test in drush cr conditions: render cache can serve stale HTML that includes old or missing schema blocks. After any preprocess or template change, always clear caches before running validation.
Conclusion
TechArticle and SoftwareApplication are the correct schema types for the content that developer blogs actually publish, yet most developer sites fall back to the generic Article type out of habit. Implementing these types takes one afternoon: add the relevant custom fields to your Drupal content types, map them to JSON-LD in a preprocess hook or twig template, and validate the output with Google's Rich Results Test. The payoff is more precise signals to search engines, potential eligibility for richer SERP features, and a structured data foundation that accurately represents what your site is.