page_structured_data 1.0.10 → 1.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58a57c3e6e31ccb80b40dcdc78ccc467b18bb6f377982f70eadd2d84a20f8308
4
- data.tar.gz: e7b26474f2cdcfc6238c67c39ea87072965b7a07857692068267c4542a4a997a
3
+ metadata.gz: 885b125f1d2fed94a1e0bba7d2c70877f0dba9c5f4ebde59c52f970f2fc4fce4
4
+ data.tar.gz: 513764c63a8dae5e609d2e92ea1e99dee3fe0555a3727afa24c3d2afdcc64ffd
5
5
  SHA512:
6
- metadata.gz: eb867392c1857beb772a52294d82ef8229551429b9655c93a79fec3a1193cfba6ce14eeec5bb42741734f938f611a05ce9029120ec9b9936c31bb0060d4b4b47
7
- data.tar.gz: 18dcb10a552cf8cec3dd2ca3ec9300a40024c808f491f03a04ab5fe7090350516b46b347fc182ab930ac4ec6c91667f8061f1de3d95f793b0c3cf35f06c63aba
6
+ metadata.gz: 1e4530a70cf06b4d81cab2322759500bdb29acb31ef8c33846f40d9095cb981c2272feb0fe6c57b6b5c363915e59ab7300071d618799459d2e34317bfc1434e8
7
+ data.tar.gz: 6a900ab594f816e1b2edd833a350a15c0837503af68f3efa0a475ac723e05b21bdc66cbad39384c680a2dfab4dfd173137dd9a05f245f236df450e757f162f98
data/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ All notable changes to this project are documented here.
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.0.12 - 2026-05-06
8
+
9
+ - Add reusable interaction statistics for article-like page types.
10
+ - Add `PageStructuredData::PageTypes::DiscussionForumPosting` for forum-style posts.
11
+ - Render Twitter Card meta tags with `name` attributes.
12
+ - Update gem metadata to mention Organization and WebSite JSON-LD support.
13
+
14
+ ## 1.0.11 - 2026-05-06
15
+
16
+ - Add optional `description` and `founder` support to organization page types.
17
+
7
18
  ## 1.0.10 - 2026-05-06
8
19
 
9
20
  - Add JSON-LD escaping coverage for organization page types.
data/README.md CHANGED
@@ -14,6 +14,8 @@ It helps Rails applications render:
14
14
  - Google-compatible JSON-LD structured data
15
15
  - Breadcrumb structured data
16
16
  - Article structured data for `BlogPosting` and `NewsArticle`
17
+ - Discussion forum post structured data
18
+ - Interaction statistics for public engagement counts
17
19
  - Organization and WebSite structured data
18
20
 
19
21
  ## Requirements
@@ -141,6 +143,7 @@ PageStructuredData includes page types for:
141
143
 
142
144
  - [`BlogPosting`](https://schema.org/BlogPosting)
143
145
  - [`NewsArticle`](https://schema.org/NewsArticle)
146
+ - [`DiscussionForumPosting`](https://schema.org/DiscussionForumPosting)
144
147
  - [`Organization`](https://schema.org/Organization)
145
148
  - [`WebSite`](https://schema.org/WebSite)
146
149
 
@@ -173,14 +176,56 @@ article_page_type = PageStructuredData::PageTypes::BlogPosting.new(
173
176
 
174
177
  For news pages, use `PageStructuredData::PageTypes::NewsArticle` with the same arguments.
175
178
 
179
+ Article-like page types can include public engagement counts as interaction statistics:
180
+
181
+ ```ruby
182
+ article_page_type = PageStructuredData::PageTypes::BlogPosting.new(
183
+ headline: @article.title,
184
+ published_at: @article.published_at,
185
+ updated_at: @article.updated_at,
186
+ likes_count: @article.likes_count,
187
+ comments_count: @article.comments_count,
188
+ shares_count: @article.shares_count
189
+ )
190
+ ```
191
+
192
+ Only include engagement counts that are public and visible on the rendered page.
193
+
194
+ Use `DiscussionForumPosting` when the current page represents a public user-authored forum or community post:
195
+
196
+ ```ruby
197
+ forum_post_page_type = PageStructuredData::PageTypes::DiscussionForumPosting.new(
198
+ headline: @post.title,
199
+ text: @post.content_plaintext,
200
+ url: post_url(@post),
201
+ published_at: @post.created_at,
202
+ updated_at: @post.updated_at,
203
+ authors: [
204
+ {
205
+ name: @post.user.name,
206
+ url: user_url(@post.user)
207
+ }
208
+ ],
209
+ interaction_statistics: [
210
+ PageStructuredData::PageTypes::InteractionStatistic.comment(@post.comments_count)
211
+ ]
212
+ )
213
+ ```
214
+
176
215
  Use `Organization` when the current page represents an organization:
177
216
 
178
217
  ```ruby
179
218
  organization_page_type = PageStructuredData::PageTypes::Organization.new(
180
219
  name: "RocketApex",
181
220
  url: "https://rocketapex.com",
221
+ description: "Open source projects from RocketApex",
182
222
  logo: "https://rocketapex.com/logo.png",
183
223
  same_as: ["https://github.com/RocketApex"],
224
+ founder: {
225
+ "@type": "Person",
226
+ name: "Jane Doe",
227
+ url: "https://example.com/jane"
228
+ },
184
229
  parent_organization: {
185
230
  name: "Parent Org",
186
231
  url: "https://parent.example"
@@ -266,7 +311,15 @@ PageStructuredData::PageTypes::BlogPosting.new(
266
311
  published_at:,
267
312
  updated_at:,
268
313
  images: [],
269
- authors: []
314
+ authors: [],
315
+ image: nil,
316
+ article_body: nil,
317
+ text: nil,
318
+ url: nil,
319
+ interaction_statistics: [],
320
+ likes_count: nil,
321
+ comments_count: nil,
322
+ shares_count: nil
270
323
  )
271
324
  ```
272
325
 
@@ -276,30 +329,86 @@ PageStructuredData::PageTypes::NewsArticle.new(
276
329
  published_at:,
277
330
  updated_at:,
278
331
  images: [],
279
- authors: []
332
+ authors: [],
333
+ image: nil,
334
+ article_body: nil,
335
+ text: nil,
336
+ url: nil,
337
+ interaction_statistics: [],
338
+ likes_count: nil,
339
+ comments_count: nil,
340
+ shares_count: nil
280
341
  )
281
342
  ```
282
343
 
283
344
  `authors` should be an array of hashes with `:name` and `:url` keys.
345
+ `image` is a convenience option for one image URL. Use `images` when passing multiple image URLs.
346
+ `text` is an alias for `article_body`.
347
+ `interaction_statistics` should be an array of `PageStructuredData::PageTypes::InteractionStatistic` objects or schema-compatible hashes.
284
348
 
285
349
  Important methods:
286
350
 
287
351
  - `to_h`: returns a structured hash for article JSON-LD.
288
352
  - `json_ld`: returns an article JSON-LD script tag.
289
353
 
354
+ ```ruby
355
+ PageStructuredData::PageTypes::DiscussionForumPosting.new(
356
+ headline:,
357
+ published_at:,
358
+ updated_at:,
359
+ images: [],
360
+ authors: [],
361
+ image: nil,
362
+ article_body: nil,
363
+ text: nil,
364
+ url: nil,
365
+ interaction_statistics: [],
366
+ likes_count: nil,
367
+ comments_count: nil,
368
+ shares_count: nil
369
+ )
370
+ ```
371
+
372
+ ### Interaction Statistics
373
+
374
+ ```ruby
375
+ PageStructuredData::PageTypes::InteractionStatistic.new(
376
+ interaction_type: :like,
377
+ user_interaction_count: 42,
378
+ interaction_service: nil
379
+ )
380
+ ```
381
+
382
+ Convenience constructors are also available:
383
+
384
+ ```ruby
385
+ PageStructuredData::PageTypes::InteractionStatistic.like(42)
386
+ PageStructuredData::PageTypes::InteractionStatistic.comment(12)
387
+ PageStructuredData::PageTypes::InteractionStatistic.share(7)
388
+ ```
389
+
390
+ Supported shorthand interaction types are `:like`, `:comment`, and `:share`. Custom schema.org action types can be passed as strings or hashes.
391
+
392
+ Important methods:
393
+
394
+ - `to_h`: returns a structured hash for `InteractionCounter` JSON-LD.
395
+
290
396
  ### Organization Page Type
291
397
 
292
398
  ```ruby
293
399
  PageStructuredData::PageTypes::Organization.new(
294
400
  name:,
295
401
  url:,
402
+ description: nil,
296
403
  logo: nil,
297
404
  same_as: [],
298
- parent_organization: nil
405
+ parent_organization: nil,
406
+ founder: nil
299
407
  )
300
408
  ```
301
409
 
302
410
  `parent_organization` should be a hash with `:name` and `:url` keys.
411
+ `founder` should be a hash or another object that responds to `to_h`.
303
412
 
304
413
  Important methods:
305
414
 
@@ -4,18 +4,26 @@ module PageStructuredData
4
4
  module PageTypes
5
5
  # Shared structured data for schema.org article-like page types.
6
6
  class Article
7
- attr_reader :headline, :images, :published_at, :updated_at, :authors
7
+ attr_reader :headline, :images, :published_at, :updated_at, :authors, :article_body, :url,
8
+ :interaction_statistics, :likes_count, :comments_count, :shares_count
8
9
 
9
- def initialize(headline:, published_at:, updated_at:, images: [], authors: [])
10
+ def initialize(headline:, published_at:, updated_at:, images: [], authors: [], image: nil, article_body: nil, text: nil,
11
+ url: nil, interaction_statistics: [], likes_count: nil, comments_count: nil, shares_count: nil)
10
12
  @headline = headline
11
- @images = images
13
+ @images = image.present? ? Array(image) : Array(images)
12
14
  @published_at = published_at
13
15
  @updated_at = updated_at
14
- @authors = authors
16
+ @authors = Array(authors)
17
+ @article_body = article_body || text
18
+ @url = url
19
+ @interaction_statistics = Array(interaction_statistics)
20
+ @likes_count = likes_count
21
+ @comments_count = comments_count
22
+ @shares_count = shares_count
15
23
  end
16
24
 
17
25
  def to_h
18
- {
26
+ node = {
19
27
  '@context': 'https://schema.org',
20
28
  '@type': schema_type,
21
29
  headline: headline,
@@ -30,6 +38,12 @@ module PageStructuredData
30
38
  }
31
39
  end,
32
40
  }
41
+
42
+ node[:articleBody] = article_body if article_body.present?
43
+ node[:url] = url if url.present?
44
+ node[:interactionStatistic] = interaction_statistics_to_h if interaction_statistics_to_h.any?
45
+
46
+ node
33
47
  end
34
48
 
35
49
  def json_ld
@@ -45,6 +59,34 @@ module PageStructuredData
45
59
  def schema_type
46
60
  raise NotImplementedError, "#{self.class.name} must define #schema_type"
47
61
  end
62
+
63
+ def interaction_statistics_to_h
64
+ @interaction_statistics_to_h ||= all_interaction_statistics.map do |interaction_statistic|
65
+ if interaction_statistic.respond_to?(:to_h)
66
+ interaction_statistic.to_h
67
+ else
68
+ interaction_statistic
69
+ end
70
+ end
71
+ end
72
+
73
+ def all_interaction_statistics
74
+ interaction_statistics + count_interaction_statistics
75
+ end
76
+
77
+ def count_interaction_statistics
78
+ [
79
+ count_interaction_statistic(:like, likes_count),
80
+ count_interaction_statistic(:comment, comments_count),
81
+ count_interaction_statistic(:share, shares_count),
82
+ ].compact
83
+ end
84
+
85
+ def count_interaction_statistic(interaction_type, count)
86
+ return if count.nil?
87
+
88
+ InteractionStatistic.new(interaction_type: interaction_type, user_interaction_count: count)
89
+ end
48
90
  end
49
91
  end
50
92
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageStructuredData
4
+ module PageTypes
5
+ # schema.org structured data for discussion forum posts.
6
+ class DiscussionForumPosting < Article
7
+ private
8
+
9
+ def schema_type
10
+ 'DiscussionForumPosting'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PageStructuredData
4
+ module PageTypes
5
+ # schema.org InteractionCounter structured data.
6
+ class InteractionStatistic
7
+ ACTION_TYPES = {
8
+ like: 'LikeAction',
9
+ likes: 'LikeAction',
10
+ comment: 'CommentAction',
11
+ comments: 'CommentAction',
12
+ share: 'ShareAction',
13
+ shares: 'ShareAction',
14
+ }.freeze
15
+
16
+ attr_reader :interaction_type, :user_interaction_count, :interaction_service
17
+
18
+ def self.like(user_interaction_count)
19
+ new(interaction_type: :like, user_interaction_count: user_interaction_count)
20
+ end
21
+
22
+ def self.comment(user_interaction_count)
23
+ new(interaction_type: :comment, user_interaction_count: user_interaction_count)
24
+ end
25
+
26
+ def self.share(user_interaction_count)
27
+ new(interaction_type: :share, user_interaction_count: user_interaction_count)
28
+ end
29
+
30
+ def initialize(interaction_type:, user_interaction_count:, interaction_service: nil)
31
+ @interaction_type = interaction_type
32
+ @user_interaction_count = user_interaction_count
33
+ @interaction_service = interaction_service
34
+ end
35
+
36
+ def to_h
37
+ node = {
38
+ '@type': 'InteractionCounter',
39
+ interactionType: interaction_type_to_h,
40
+ userInteractionCount: user_interaction_count,
41
+ }
42
+
43
+ node[:interactionService] = object_to_h(interaction_service) if interaction_service.present?
44
+
45
+ node
46
+ end
47
+
48
+ private
49
+
50
+ def interaction_type_to_h
51
+ return object_to_h(interaction_type) if interaction_type.respond_to?(:to_h)
52
+
53
+ type = ACTION_TYPES.fetch(interaction_type.to_s.to_sym, interaction_type.to_s)
54
+
55
+ { '@type': type }
56
+ end
57
+
58
+ def object_to_h(object)
59
+ return object.to_h if object.respond_to?(:to_h)
60
+
61
+ object
62
+ end
63
+ end
64
+ end
65
+ end
@@ -4,14 +4,16 @@ module PageStructuredData
4
4
  module PageTypes
5
5
  # Organization structured data for a page
6
6
  class Organization
7
- attr_reader :name, :url, :logo, :same_as, :parent_organization
7
+ attr_reader :name, :url, :description, :logo, :same_as, :parent_organization, :founder
8
8
 
9
- def initialize(name:, url:, logo: nil, same_as: [], parent_organization: nil)
9
+ def initialize(name:, url:, description: nil, logo: nil, same_as: [], parent_organization: nil, founder: nil)
10
10
  @name = name
11
11
  @url = url
12
+ @description = description
12
13
  @logo = logo
13
14
  @same_as = same_as
14
15
  @parent_organization = parent_organization
16
+ @founder = founder
15
17
  end
16
18
 
17
19
  def to_h # rubocop:disable Metrics/MethodLength
@@ -22,8 +24,10 @@ module PageStructuredData
22
24
 
23
25
  node[:name] = name
24
26
  node[:url] = url
27
+ node[:description] = description if description.present?
25
28
  node[:logo] = logo if logo.present?
26
29
  node[:sameAs] = same_as if same_as.present?
30
+ node[:founder] = founder_to_h if founder.present?
27
31
 
28
32
  if parent_organization.present?
29
33
  node[:parentOrganization] = {
@@ -43,6 +47,14 @@ module PageStructuredData
43
47
  </script>
44
48
  )
45
49
  end
50
+
51
+ private
52
+
53
+ def founder_to_h
54
+ return founder.to_h if founder.respond_to?(:to_h)
55
+
56
+ founder
57
+ end
46
58
  end
47
59
  end
48
60
  end
@@ -18,9 +18,9 @@
18
18
  <meta property="og:description" content="<%= description %>">
19
19
  <meta property="og:image" content="<%= image %>">
20
20
 
21
- <meta property="twitter:card" content="summary_large_image">
22
- <meta property="twitter:title" content="<%= title %>">
23
- <meta property="twitter:description" content="<%= description %>">
24
- <meta property="twitter:image" content="<%= image %>">
21
+ <meta name="twitter:card" content="summary_large_image">
22
+ <meta name="twitter:title" content="<%= title %>">
23
+ <meta name="twitter:description" content="<%= description %>">
24
+ <meta name="twitter:image" content="<%= image %>">
25
25
 
26
26
  <%= page&.json_lds&.html_safe %>
@@ -1,3 +1,3 @@
1
1
  module PageStructuredData
2
- VERSION = "1.0.10"
2
+ VERSION = "1.0.12"
3
3
  end
@@ -2,9 +2,11 @@ require "page_structured_data/version"
2
2
  require "page_structured_data/engine"
3
3
  require_relative "../app/src/page_structured_data/anchors"
4
4
  require_relative "../app/src/page_structured_data/breadcrumbs"
5
+ require_relative "../app/src/page_structured_data/page_types/interaction_statistic"
5
6
  require_relative "../app/src/page_structured_data/page_types/article"
6
7
  require_relative "../app/src/page_structured_data/page_types/blog_posting"
7
8
  require_relative "../app/src/page_structured_data/page_types/news_article"
9
+ require_relative "../app/src/page_structured_data/page_types/discussion_forum_posting"
8
10
  require_relative "../app/src/page_structured_data/page_types/organization"
9
11
  require_relative "../app/src/page_structured_data/page_types/web_site"
10
12
  require_relative "../app/src/page_structured_data/page"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: page_structured_data
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.10
4
+ version: 1.0.12
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jey Geethan
@@ -32,7 +32,8 @@ dependencies:
32
32
  version: '9.0'
33
33
  description: PageStructuredData gives Rails applications a small page object and view
34
34
  partial for rendering page titles, basic meta tags, Open Graph tags, Twitter card
35
- tags, breadcrumb JSON-LD, and article JSON-LD.
35
+ tags, breadcrumb JSON-LD, article and forum post JSON-LD, Organization JSON-LD,
36
+ WebSite JSON-LD, and public interaction statistics.
36
37
  email:
37
38
  - opensource@rocketapex.com
38
39
  executables: []
@@ -48,6 +49,8 @@ files:
48
49
  - app/src/page_structured_data/page.rb
49
50
  - app/src/page_structured_data/page_types/article.rb
50
51
  - app/src/page_structured_data/page_types/blog_posting.rb
52
+ - app/src/page_structured_data/page_types/discussion_forum_posting.rb
53
+ - app/src/page_structured_data/page_types/interaction_statistic.rb
51
54
  - app/src/page_structured_data/page_types/news_article.rb
52
55
  - app/src/page_structured_data/page_types/organization.rb
53
56
  - app/src/page_structured_data/page_types/web_site.rb