Discussion Forum Schema Framework

Use conditional-logic schema generation to keep Discussion Forum markup aligned with real page state and avoid stale or invalid static templates.

When To Use It

Google Search CentralDiscussion Forum documentation

Use for user-generated, forum-style threads where a primary post and its replies are publicly visible: community forums, generic forum platforms, and social media platforms with user posts. Do not use it for publisher-written articles (even with user comments) or product reviews, and use QAPage markup instead when the forum follows a question-and-answer pattern.

Key Implementation Documentation Highlights

  • Types: DiscussionForumPosting, or SocialMediaPosting, which has the same requirements.
  • Required on the post: author (Person or Organization, with author.name) and datePublished (ISO 8601), plus at least one of text, image, or video. The content requirement does not apply when the posting represents a post on another page.
  • Recommended on the post: author.url, comment, commentCount, dateModified, headline, interactionStatistic, isPartOf, mainEntityOfPage, sharedContent, and url. Conditional: creativeWorkStatus set to Deleted for a removed post kept for context, and digitalSourceType for content generated by an LLM (TrainedAlgorithmicMediaDigitalSource) or a simpler bot (AlgorithmicMediaDigitalSource).
  • Each Comment has the same shape: author with name and datePublished are required, plus one of text, image, or video. Nest replies as a tree that mirrors the forum's threading, in the order they appear on the page.
  • interactionStatistic.interactionType must be LikeAction, DislikeAction, ViewAction, CommentAction, ReplyAction, or ShareAction, with an integer userInteractionCount.
  • image holds inline images only: no default, icon, placeholder, or author images. headline should not duplicate or truncate the text when the post has no separate title.
  • For multi-page threads, later pages include the original post, with url set to the first page URL.
  • Use the Rich Results Test and URL Inspection to validate rendering, eligibility, and crawlability.

Rules the framework enforces before it emits a posting:

  • Q&A threads return nothing here and go to the QAPage framework instead.
  • Posts and comments without an author name, a valid ISO 8601 date-time, or any text, image, or video are dropped; a dropped comment takes its replies with it.
  • Interaction counts map to the six supported action types and must be non-negative integers; anything else is skipped.
  • commentCount is never lower than the number of comments in the markup, and a dateModified earlier than datePublished is dropped.

Template Approach

const pageData = {
  title: "Rendering delays after migrating to a JS framework",
  body: "Since moving our product pages to client-side rendering, new URLs take weeks to show rendered content.",
  url: "https://forum.example.com/t/rendering-delays-after-migration/4821",
  postedAt: "2026-09-28T10:14:00-04:00",
  authorName: "mkhan",
  authorUrl: "https://forum.example.com/u/mkhan"
};

const staticTemplate = {
  "@context": "https://schema.org",
  "@type": "DiscussionForumPosting",
  "headline": pageData.title,
  "text": pageData.body,
  "url": pageData.url,
  "datePublished": pageData.postedAt,
  "author": { "@type": "Person", "name": pageData.authorName, "url": pageData.authorUrl }
};

Conditional-logic Framework

const ISO_DATE = /^(\d{4})-(\d{2})-(\d{2})$/;
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})T([01]\d|2[0-3]):[0-5]\d(:[0-5]\d(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/;

function isAbsoluteUrl(value) {
  if (typeof value !== "string") return false;
  try {
    const url = new URL(value);
    return (url.protocol === "https:" || url.protocol === "http:") && Boolean(url.hostname);
  } catch {
    return false;
  }
}

function isRealDate(y, m, d) {
  const date = new Date(Date.UTC(Number(y), Number(m) - 1, Number(d)));
  return date.getUTCMonth() === Number(m) - 1 && date.getUTCDate() === Number(d);
}

// Calendar date (YYYY-MM-DD); a full date-time is cut to its date part.
function toIsoDate(value) {
  if (typeof value !== "string") return null;
  const v = value.trim();
  const m = v.match(ISO_DATE) || v.match(ISO_DATE_TIME);
  return m && isRealDate(m[1], m[2], m[3]) ? v.slice(0, 10) : null;
}

// Date-time with Z or an offset (kept as written), or a plain date.
// A date-time without a timezone is rejected instead of guessed.
function toIsoDateTime(value) {
  if (typeof value !== "string") return null;
  const v = value.trim();
  const m = v.match(ISO_DATE) || v.match(ISO_DATE_TIME);
  return m && isRealDate(m[1], m[2], m[3]) ? v : null;
}

// Drop null, undefined, "", [] and {} recursively.
function compact(value) {
  if (Array.isArray(value)) {
    const items = value.map(compact).filter((v) => v !== undefined);
    return items.length ? items : undefined;
  }
  if (value && typeof value === "object") {
    const out = {};
    for (const [key, v] of Object.entries(value)) {
      const c = compact(v);
      if (c !== undefined) out[key] = c;
    }
    return Object.keys(out).length ? out : undefined;
  }
  if (value === null || value === undefined || value === "") return undefined;
  return value;
}

// Page gates: indexable, self-canonical, and the marked-up content is visible.
function passesPageGates(source) {
  if (!source.isIndexable || !source.isContentVisible) return false;
  if (!isAbsoluteUrl(source.url)) return false;
  return !source.canonicalUrl || source.canonicalUrl === source.url;
}

const INTERACTION_TYPES = {
  likes: "https://schema.org/LikeAction",
  dislikes: "https://schema.org/DislikeAction",
  views: "https://schema.org/ViewAction",
  comments: "https://schema.org/CommentAction",
  replies: "https://schema.org/ReplyAction",
  shares: "https://schema.org/ShareAction"
};
const DIGITAL_SOURCE_TYPES = {
  llm: "https://schema.org/TrainedAlgorithmicMediaDigitalSource",
  bot: "https://schema.org/AlgorithmicMediaDigitalSource"
};

// Q&A threads belong in QAPage markup; social platforms use SocialMediaPosting.
function chooseForumPostingType(source) {
  if (source.isQuestionAnswerThread) return null;
  return source.platform === "social" ? "SocialMediaPosting" : "DiscussionForumPosting";
}

function buildAuthor(author) {
  if (!author || !author.name) return null;
  return {
    "@type": author.isOrganization ? "Organization" : "Person",
    "name": author.name,
    "url": isAbsoluteUrl(author.url) ? author.url : null
  };
}

function buildStats(stats) {
  return Object.entries(stats || {})
    .filter(([key, count]) => INTERACTION_TYPES[key] && Number.isInteger(count) && count >= 0)
    .map(([key, count]) => ({ "@type": "InteractionCounter", "interactionType": INTERACTION_TYPES[key], "userInteractionCount": count }));
}

// Shared fields for posts and comments. Returns null when a required field is missing.
function buildEntry(entry, { allowNoContent = false } = {}) {
  const author = buildAuthor(entry.author);
  const datePublished = toIsoDateTime(entry.datePublished);
  if (!author || !datePublished) return null;

  // Inline media only: never avatars, icons or placeholders.
  const image = (entry.images || []).filter(isAbsoluteUrl);
  const text = (entry.text || "").trim();
  if (!text && !image.length && !entry.video && !allowNoContent) return null;

  let dateModified = toIsoDateTime(entry.dateModified);
  if (dateModified && Date.parse(dateModified) < Date.parse(datePublished)) dateModified = null;

  const comment = (entry.comments || []).map((c) => buildEntry(c)).filter(Boolean)
    .map((c) => ({ "@type": "Comment", ...c })); // replies stay nested, in page order
  return {
    "author": author,
    "datePublished": datePublished,
    "dateModified": dateModified,
    "text": text,
    "image": image,
    "video": entry.video || null,
    "url": isAbsoluteUrl(entry.url) ? entry.url : null,
    "creativeWorkStatus": entry.isDeleted ? "Deleted" : null,
    "digitalSourceType": DIGITAL_SOURCE_TYPES[entry.generatedBy] || null,
    "interactionStatistic": buildStats(entry.stats),
    "comment": comment,
    "commentCount": Number.isInteger(entry.commentCount) && entry.commentCount >= comment.length ? entry.commentCount : null
  };
}

function countComments(comments) {
  return (comments || []).reduce((n, c) => n + 1 + countComments(c.comment), 0);
}

function buildDiscussionForumSchema(source) {
  if (!passesPageGates(source)) return null;
  const type = chooseForumPostingType(source);
  if (!type) return null;

  // Text, image or video is required unless the posting represents a post on another page.
  const postUrl = isAbsoluteUrl(source.postUrl) ? source.postUrl : source.url;
  const entry = buildEntry(source.post || {}, { allowNoContent: postUrl !== source.url });
  if (!entry) return null;

  const post = source.post;
  const headline = (post.headline || "").trim();
  // A headline that repeats or truncates the text adds nothing: drop it.
  const keepHeadline = headline && !entry.text.startsWith(headline.replace(/(\.\.\.|…)$/, ""));
  const marked = countComments(entry.comment);

  return compact({
    "@context": "https://schema.org",
    "@type": type,
    "headline": keepHeadline ? headline : null,
    ...entry,
    "commentCount": Math.max(entry.commentCount || 0, marked) || null,
    "url": postUrl, // first page URL for multi-page threads
    "mainEntityOfPage": source.isSinglePostPage ? source.url : null,
    "isPartOf": isAbsoluteUrl(source.subforumUrl) ? source.subforumUrl : null
  }) || null;
}

// Usage
// const source = {
//   "isIndexable": true,
//   "isContentVisible": true,
//   "url": "https://forum.example.com/t/rendering-delays-after-migration/4821",
//   "canonicalUrl": "https://forum.example.com/t/rendering-delays-after-migration/4821",
//   "isSinglePostPage": true,
//   "subforumUrl": "https://forum.example.com/c/technical-seo",
//   "post": {
//     "headline": "Rendering delays after migrating to a JS framework",
//     "text": "Since moving our product pages to client-side rendering, new URLs take two to three weeks to show rendered content in URL Inspection. Has anyone measured this?",
//     "images": [
//       "https://forum.example.com/uploads/render-timeline.png"
//     ],
//     "author": {
//       "name": "mkhan",
//       "url": "https://forum.example.com/u/mkhan"
//     },
//     "datePublished": "2026-09-28T10:14:00-04:00",
//     "dateModified": "2026-09-28T11:02:00-04:00",
//     "stats": {
//       "likes": 18,
//       "views": 1240
//     },
//     "commentCount": 3,
//     "comments": [
//       {
//         "text": "We saw the same pattern. Server-rendering the main content cut it to a few days.",
//         "author": {
//           "name": "priya_r",
//           "url": "https://forum.example.com/u/priya_r"
//         },
//         "datePublished": "2026-09-28T12:40:00-04:00",
//         "url": "https://forum.example.com/t/rendering-delays-after-migration/4821/2",
//         "stats": {
//           "likes": 9
//         },
//         "comments": [
//           {
//             "text": "Did you keep hydration for the reviews widget?",
//             "author": {
//               "name": "mkhan",
//               "url": "https://forum.example.com/u/mkhan"
//             },
//             "datePublished": "2026-09-28T13:05:00-04:00",
//             "url": "https://forum.example.com/t/rendering-delays-after-migration/4821/3"
//           }
//         ]
//       },
//       {
//         "text": "Check whether your sitemap lastmod values changed during the migration.",
//         "author": {
//           "name": "dlee",
//           "url": "https://forum.example.com/u/dlee"
//         },
//         "datePublished": "2026-09-29T08:20:00-04:00",
//         "url": "https://forum.example.com/t/rendering-delays-after-migration/4821/4"
//       }
//     ]
//   }
// };
// const jsonLd = buildDiscussionForumSchema(source); // null means: emit nothing

The Output As An Entity Graph

The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.

Why Conditional Logic Is Better Than Static Templates

  • Builds the comment tree recursively from the thread data, so nesting and order match what users see.
  • Handles deleted posts, edited posts, bot or LLM generated replies, and multi-page threads without separate templates.
  • Keeps interaction statistics limited to supported action types and real counts.
  • Routes question-and-answer threads to QAPage markup instead of emitting the wrong type.

Property Reference

What Google's Discussion forum documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.

Required, one of: To represent the content of the post, include one of text, image, or video. Not required if representing a post on another page (with an external url).

Required, one of: In comment: a comment must include one of text, image, or video.

Discussion forum properties in Google's documentation
PropertyGoogleExpectsNotes
authorRequiredPerson or Organization, can repeatInformation about the author of the post.
author.nameRequiredTextThe name of the author of the post.
author.urlRecommendedURLA link to a web page that uniquely identifies the author of the post, most likely a profile page of the forum.
datePublishedRequiredDateTimeThe date and time the post was made, in ISO 8601 format.
textOne ofTextAny text in the post, if applicable.
imageOne ofImageObject or URL, can repeatInline images within the post. Do not use default, icon, placeholder, or author images.
videoOne ofVideoObject, can repeatAny inline videos within the post, if applicable.
commentRecommendedComment, can repeatComments about or responses to the post, in the order they appear on the page.
comment.authorRequiredPerson or Organization, can repeatInformation about the author of the comment.
comment.datePublishedRequiredDateTimeThe date and time the comment was made, in ISO 8601 format.
comment.textOne ofTextAny text in the comment, if applicable.
comment.imageOne ofImageObject or URL, can repeatInline images within the comment, if applicable.
comment.videoOne ofVideoObject, can repeatInline videos within the comment, if applicable.
comment.commentRecommendedComment, can repeatReplies to this comment, if applicable.
comment.commentCountRecommendedIntegerThe number of replies to this comment, if applicable.
comment.creativeWorkStatusRecommendedTextSet creativeWorkStatus to Deleted when the comment was deleted but remains for context or threading. One of: Deleted.
comment.dateModifiedRecommendedDateTimeWhen the comment was edited, in ISO 8601 format, if applicable.
comment.digitalSourceTypeRecommendedEnumUse when the comment was generated by an automated system (LLM or simpler bot). One of: TrainedAlgorithmicMediaDigitalSource, AlgorithmicMediaDigitalSource.
comment.interactionStatisticRecommendedInteractionCounter, can repeatUser statistics applied to the comment, if applicable.
comment.sharedContentRecommendedCreativeWork, WebPage, ImageObject, VideoObject, DiscussionForumPosting or CommentThe primary shared content in the comment, if applicable.
comment.urlRecommendedURLThe URL to this specific comment on the page, if applicable.
commentCountRecommendedIntegerThe number of comments about this post, if applicable.
creativeWorkStatusRecommendedTextSet creativeWorkStatus to Deleted when the post has been deleted but remains for context or threading. One of: Deleted.
dateModifiedRecommendedDateTimeThe date and time the post was edited, in ISO 8601 format, if applicable.
digitalSourceTypeRecommendedEnumUse when the post was generated by an automated system. One of: TrainedAlgorithmicMediaDigitalSource, AlgorithmicMediaDigitalSource.
headlineRecommendedTextThe title of the post. Don't duplicate or truncate the post text into a headline.
interactionStatisticRecommendedInteractionCounter, can repeatUser statistics applied to the post, if applicable.
interactionStatistic.interactionTypeRequiredEnumThe subtype of Action the count refers to. One of: LikeAction, DislikeAction, ViewAction, CommentAction, ReplyAction, ShareAction.
interactionStatistic.userInteractionCountRequiredIntegerThe number of times this interaction was performed.
isPartOfRecommendedCreativeWork or URLThe primary source of the post if it occurs on a particular part of the website (for example a subforum).
mainEntityOfPageRecommendedWebPage or URLUse when the page is primarily about this single post.
sharedContentRecommendedCreativeWork, WebPage, ImageObject, VideoObject, DiscussionForumPosting or CommentThe primary shared content in the post, if applicable.
urlRecommendedURLThe canonical URL of the discussion (first page URL for multi-page threads).

Explore Other Schema Framework Pages

Limited Education Q&A Use for educational question-answer pages where a single canonical answer and supporting context are visible. Quiz Open Employer Aggregate Rating Use on pages that publish employer rating aggregates from a clear and policy-compliant review source. EmployerAggregateRating Open Retired Estimated Salary Use for compensation insight pages that present transparent salary ranges for specific occupations and locations. Occupation Open Event Use for upcoming events with a real-world or virtual occurrence, schedule, and location context. Event Open Retired Fact Check Use for fact-checking pages that evaluate a specific claim with a transparent rating methodology. ClaimReview Open Retired Home Activities Use for virtual classes and activities where participation details and timing are clearly listed. Event Open Retired How-To Use for instructional content that teaches a process through ordered steps and actionable detail. HowTo Open Image Metadata Use on image pages and image hosts that expose ownership and licensing information to search engines. ImageObject Open Job Posting Use for active job listings that include role details, hiring entity, and location/salary context. JobPosting Open Retired Learning Video Use for educational videos where the learning objective and pedagogical structure are explicit. LearningResource Open Local Business Use for physical businesses serving local customers with publicly verifiable contact and location details. LocalBusiness Open Loyalty Program Use to publish a loyalty program and its tiers once at the organization level, so member prices and member shipping can reference them. MemberProgram Open Math Solvers Use for pages that solve mathematical expressions and present step-by-step computational guidance. MathSolver Open Merchant Listing Use on pages where a product can be bought, with price, availability, shipping and return details. Product Open Limited Movie Use for movie detail pages with canonical film information and transparent metadata attribution. Movie Open Organization Use for official organization pages to clarify brand identity and authoritative entity signals. Organization Open Retired Practice Problems Use for educational practice content that presents solvable problems and answer validation structure. Quiz Open Product Snippet Use for product pages where pricing, availability, and product identity are visible to users. Product Open Product Variants Use when one product comes in variants such as size or color, grouped with ProductGroup. ProductGroup Open Profile Page Use for creator or contributor profile pages where identity attributes and ownership are explicit. ProfilePage Open Q&A Use for pages where users submit questions and community or editorial answers are displayed. QAPage Open Recipe Use for recipe pages with complete ingredients, instructions, and preparation details. Recipe Open Return Policy Use to publish a merchant return policy once at the organization level, with offer-level overrides. MerchantReturnPolicy Open Review Snippet Use when a page publishes genuine reviews tied to a clearly identified product, service, or entity. Review Open Shipping Policy Use to publish shipping rates, delivery times and destinations once at the organization level. ShippingService Open Site Name Use on the home page so Google shows the preferred site name in search results. WebSite Open Retired Sitelinks Search Box Use on sites with a functioning internal search endpoint that can accept URL query parameters. WebSite Open Software App (Beta) Use for software product pages that describe platform support and application category clearly. SoftwareApplication Open Limited Speakable Use for news-style pages where specific text segments are appropriate for text-to-speech playback. SpeakableSpecification Open Subscription and Paywalled Content Use for pages with restricted sections so Google can distinguish free previews from protected content. CreativeWork Open Limited Vacation Rental Use for rental property detail pages where booking-relevant amenities and location signals are explicit. VacationRental Open Retired Vehicle Listing Use for inventory pages selling vehicles with standardized listing fields and offer information. Car Open Video Use for pages where a primary video is embedded and key media metadata is visible on-page. VideoObject Open Article Use for editorial pages where the visible content is a complete article with a clear byline and publication timestamps. Article Open Limited Book Actions Use when your page helps users read, preview, or purchase books through clearly described actions. Book Open Breadcrumb Use on pages that belong to a clear hierarchy and display navigational trail links users can follow. BreadcrumbList Open Carousel Use when a page lists multiple related entities in a scannable sequence that can be surfaced as cards. ItemList Open Course Use for educational offerings where curriculum details, provider identity, and delivery context are explicit. Course Open Dataset Use for dataset landing pages so datasets are discoverable in Google Dataset Search with license, creator and download details. Dataset Open

Implement Discussion Forum Schema Frameworks Correctly

Get implementation support for conditional schema architecture, validation, and deployment governance.