trifle-docs 0.7.1 → 0.7.2

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: 2f5639f53f19ef35690cc1f68d2866cb05c8edc61f4c31cbe77f75c27083d27d
4
- data.tar.gz: 197207ceb689bc6e539c37dc7443a9a513f4eb135436d0a04df58d7cfbab074b
3
+ metadata.gz: 4660d54f61927575620d127f9493bc2921bf4a0395362acd2f71b4eeaee38712
4
+ data.tar.gz: c59600b536c976dbf67eaf4f99a61d214b1022b6a18795cc7ea37d9f5caf4ba7
5
5
  SHA512:
6
- metadata.gz: 4647c880321bb90b15779068c670ce28961002f638ab447ed76dfb8ea2d49d8fe312edc3402d588c7399e1d5b7a73b53288c557a6a1ff4d02589af57b8684fe1
7
- data.tar.gz: 312f3d0a696d060f7f871dee17946215fd8eb500783771f4d6fe962f4b533b5766e396b05b133aba83a52e28ce426d08348c30842b482e05412bfbd219c379e8
6
+ metadata.gz: fc4c3fa9f2d46e94c6a7a19169830b33c2601fbc2a39360945d57691332b3d3a6d00be8fdc12aa13a379595aefd655f887c02dcbd3ceda561c6431fdda1b0d2d
7
+ data.tar.gz: cd867ffc069b37ee0e513893abe097c8ed4c6a5ecdd8e7fb1d47c0fd0c33f950892024ca04ac653f3281383aab64cb3d7c88671ee5b5b8579b543b49112b3745
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- trifle-docs (0.7.1)
4
+ trifle-docs (0.7.2)
5
5
  redcarpet
6
6
  rouge
7
7
  sinatra
data/README.md CHANGED
@@ -41,6 +41,8 @@ require 'trifle/docs'
41
41
  Trifle::Docs.configure do |config|
42
42
  config.path = File.join(__dir__, 'docs')
43
43
  config.views = File.join(__dir__, 'templates')
44
+ # Optional: canonical base URL used for absolute <loc> values in sitemap.xml
45
+ # config.sitemap_base_url = ENV.fetch('TRIFLE_DOCS_SITEMAP_BASE_URL', nil)
44
46
  config.register_harvester(Trifle::Docs::Harvester::Markdown)
45
47
  config.register_harvester(Trifle::Docs::Harvester::File)
46
48
  end
@@ -110,6 +112,22 @@ There are several variables available in your template file (except `layout.erb`
110
112
  - **Caching support** - Optional caching for production environments
111
113
  - **Navigation helpers** - Automatic menu and breadcrumb generation
112
114
 
115
+ ## Sitemap URL configuration
116
+
117
+ `/sitemap.xml` emits absolute URLs. By default, Trifle::Docs uses the incoming request host (`request.base_url`).
118
+
119
+ If your app runs behind a proxy/ingress and you need a canonical public domain, set:
120
+
121
+ ```ruby
122
+ config.sitemap_base_url = 'https://docs.trifle.io'
123
+ ```
124
+
125
+ In containerized deployments you can wire this through an env var, for example:
126
+
127
+ ```ruby
128
+ config.sitemap_base_url = ENV.fetch('TRIFLE_DOCS_SITEMAP_BASE_URL', nil)
129
+ ```
130
+
113
131
  ## Harvesters
114
132
 
115
133
  Trifle::Docs supports multiple content processors:
@@ -4,65 +4,11 @@ require 'sinatra/base'
4
4
 
5
5
  module Trifle
6
6
  module Docs
7
- class App < Sinatra::Base
8
- configure do
9
- set :bind, '0.0.0.0'
10
- set :views, proc { Trifle::Docs.default.views }
11
- end
12
-
13
- get '/search' do
14
- results = Trifle::Docs.search(query: params['query'], scope: params['scope'])
15
- erb(
16
- 'search'.to_sym,
17
- {},
18
- {
19
- results: results,
20
- query: params['query'],
21
- scope: params['scope'],
22
- sitemap: Trifle::Docs.sitemap,
23
- meta: { description: 'Search' }
24
- }
25
- )
26
- end
27
-
28
- get '/llms.txt' do
29
- meta = Trifle::Docs.meta(url: 'llms.txt')
30
- return send_file(meta['path']) if meta && meta['type'] == 'file'
31
-
32
- content = Trifle::Docs::Helper::Llms.homepage_markdown
33
- halt(404, 'Not Found') if content.nil?
34
-
35
- content_type 'text/plain'
36
- content
37
- end
38
-
39
- get '/llms-full.txt' do
40
- meta = Trifle::Docs.meta(url: 'llms-full.txt')
41
- return send_file(meta['path']) if meta && meta['type'] == 'file'
42
-
43
- content = Trifle::Docs::Helper::Llms.full_markdown
44
- halt(404, 'Not Found') if content.nil? || content.strip.empty?
45
-
46
- content_type 'text/plain'
47
- content
48
- end
49
-
50
- get '/sitemap.xml' do
51
- meta = Trifle::Docs.meta(url: 'sitemap.xml')
52
- return send_file(meta['path']) if meta && meta['type'] == 'file'
53
-
54
- render_generated_sitemap
55
- end
56
-
57
- get '/*' do
58
- handle_request(params, request)
59
- end
60
-
7
+ module AppHelpers
61
8
  def markdown_requested?(request, params)
62
9
  return true if params['format'].to_s.downcase == 'md'
63
10
 
64
- accept = request.env['HTTP_ACCEPT'].to_s
65
- accept.include?('text/markdown')
11
+ request.env['HTTP_ACCEPT'].to_s.include?('text/markdown')
66
12
  end
67
13
 
68
14
  def render_markdown?(meta, request, params)
@@ -77,9 +23,9 @@ module Trifle
77
23
  meta = Trifle::Docs.meta(url: url)
78
24
  halt(404, 'Not Found') if meta.nil?
79
25
 
80
- set_vary_header unless meta['type'] == 'file'
26
+ set_vary_header unless file_meta?(meta)
81
27
  return render_markdown(meta, url) if render_markdown?(meta, request, params)
82
- return send_file(meta['path']) if meta['type'] == 'file'
28
+ return send_file(meta['path']) if file_meta?(meta)
83
29
 
84
30
  render_html(meta, url)
85
31
  end
@@ -88,8 +34,7 @@ module Trifle
88
34
  content_type markdown_content_type(request)
89
35
  Trifle::Docs::Helper::MarkdownLayout.render(
90
36
  meta: meta,
91
- raw_content: Trifle::Docs.raw_content(url: url),
92
- sitemap: Trifle::Docs.sitemap
37
+ raw_content: Trifle::Docs.raw_content(url: url)
93
38
  )
94
39
  end
95
40
 
@@ -104,7 +49,8 @@ module Trifle
104
49
  end
105
50
 
106
51
  def render_generated_sitemap
107
- content = Trifle::Docs::Helper::Sitemap.xml
52
+ base_url = Trifle::Docs.default.sitemap_base_url || request.base_url
53
+ content = Trifle::Docs::Helper::Sitemap.xml(base_url: base_url)
108
54
  halt(404, 'Not Found') if content.nil? || content.strip.empty?
109
55
 
110
56
  content_type 'application/xml'
@@ -122,6 +68,21 @@ module Trifle
122
68
  headers['Vary'] = append_vary(headers['Vary'], 'Accept')
123
69
  end
124
70
 
71
+ def render_llms_for(base_url, full:)
72
+ base_url = normalize_base_url(base_url)
73
+ url = llms_url(base_url, full: full)
74
+ meta = Trifle::Docs.meta(url: url)
75
+ return send_file(meta['path']) if file_meta?(meta)
76
+
77
+ content = llms_content(full: full, base_url: base_url)
78
+ halt(404, 'Not Found') if llms_missing?(content, full: full)
79
+
80
+ content_type 'text/plain'
81
+ content
82
+ end
83
+
84
+ private
85
+
125
86
  def append_vary(existing, value)
126
87
  values = existing.to_s.split(',').map(&:strip).reject(&:empty?)
127
88
  return value if values.empty?
@@ -130,7 +91,84 @@ module Trifle
130
91
  (values + [value]).join(', ')
131
92
  end
132
93
 
133
- private :handle_request
94
+ def normalize_base_url(base_url)
95
+ base_url.to_s.gsub(%r{^/+}, '').gsub(%r{/+$}, '')
96
+ end
97
+
98
+ def llms_url(base_url, full:)
99
+ llms_name = full ? 'llms-full.txt' : 'llms.txt'
100
+ [base_url, llms_name].reject(&:empty?).join('/')
101
+ end
102
+
103
+ def llms_content(full:, base_url:)
104
+ if full
105
+ Trifle::Docs::Helper::Llms.full_markdown(base_url: base_url)
106
+ else
107
+ Trifle::Docs::Helper::Llms.homepage_markdown(base_url: base_url)
108
+ end
109
+ end
110
+
111
+ def llms_missing?(content, full:)
112
+ return true if content.nil?
113
+ return false unless full
114
+
115
+ content.strip.empty?
116
+ end
117
+
118
+ def file_meta?(meta)
119
+ meta && meta['type'] == 'file'
120
+ end
121
+ end
122
+
123
+ class App < Sinatra::Base
124
+ configure do
125
+ set :bind, '0.0.0.0'
126
+ set :views, proc { Trifle::Docs.default.views }
127
+ end
128
+
129
+ helpers AppHelpers
130
+
131
+ get '/search' do
132
+ results = Trifle::Docs.search(query: params['query'], scope: params['scope'])
133
+ erb(
134
+ 'search'.to_sym,
135
+ {},
136
+ {
137
+ results: results,
138
+ query: params['query'],
139
+ scope: params['scope'],
140
+ sitemap: Trifle::Docs.sitemap,
141
+ meta: { description: 'Search' }
142
+ }
143
+ )
144
+ end
145
+
146
+ get '/llms.txt' do
147
+ render_llms_for('', full: false)
148
+ end
149
+
150
+ get %r{/(.+)/llms\.txt} do |path|
151
+ render_llms_for(path, full: false)
152
+ end
153
+
154
+ get '/llms-full.txt' do
155
+ render_llms_for('', full: true)
156
+ end
157
+
158
+ get %r{/(.+)/llms-full\.txt} do |path|
159
+ render_llms_for(path, full: true)
160
+ end
161
+
162
+ get '/sitemap.xml' do
163
+ meta = Trifle::Docs.meta(url: 'sitemap.xml')
164
+ return send_file(meta['path']) if meta && meta['type'] == 'file'
165
+
166
+ render_generated_sitemap
167
+ end
168
+
169
+ get '/*' do
170
+ handle_request(params, request)
171
+ end
134
172
  end
135
173
  end
136
174
  end
@@ -3,13 +3,14 @@
3
3
  module Trifle
4
4
  module Docs
5
5
  class Configuration
6
- attr_accessor :path, :views, :layout, :namespace, :cache
6
+ attr_accessor :path, :views, :layout, :namespace, :cache, :sitemap_base_url
7
7
 
8
8
  def initialize
9
9
  @harvesters = []
10
10
  @path = nil
11
11
  @namespace = nil
12
12
  @cache = true
13
+ @sitemap_base_url = nil
13
14
  end
14
15
 
15
16
  def harvester
@@ -19,6 +19,8 @@ if Object.const_defined?('Rails')
19
19
  root to: 'page#show'
20
20
  get 'llms.txt', to: 'page#llms'
21
21
  get 'llms-full.txt', to: 'page#llms_full'
22
+ get '*path/llms.txt', to: 'page#llms'
23
+ get '*path/llms-full.txt', to: 'page#llms_full'
22
24
  get 'sitemap.xml', to: 'page#sitemap'
23
25
  get 'search', to: 'page#search'
24
26
  get '*url', to: 'page#show'
@@ -71,8 +73,7 @@ if Object.const_defined?('Rails')
71
73
  def render_markdown(url:, meta:)
72
74
  render plain: Trifle::Docs::Helper::MarkdownLayout.render(
73
75
  meta: meta,
74
- raw_content: Trifle::Docs.raw_content(url: url, config: configuration),
75
- sitemap: Trifle::Docs.sitemap(config: configuration)
76
+ raw_content: Trifle::Docs.raw_content(url: url, config: configuration)
76
77
  ), content_type: markdown_content_type
77
78
  end
78
79
 
@@ -100,6 +101,10 @@ if Object.const_defined?('Rails')
100
101
  [url, wants_md]
101
102
  end
102
103
 
104
+ def llms_scope
105
+ params[:path].to_s.gsub(%r{^/+}, '').gsub(%r{/+$}, '')
106
+ end
107
+
103
108
  def render_markdown?(meta, wants_md, request)
104
109
  return false if meta['type'] == 'file'
105
110
 
@@ -166,20 +171,31 @@ if Object.const_defined?('Rails')
166
171
  end
167
172
 
168
173
  def llms
169
- render_llms('llms.txt', allow_empty: true) do
170
- Trifle::Docs::Helper::Llms.homepage_markdown(config: configuration)
174
+ base_url = llms_scope
175
+ llms_url = [base_url, 'llms.txt'].reject(&:empty?).join('/')
176
+ render_llms(llms_url, allow_empty: true) do
177
+ Trifle::Docs::Helper::Llms.homepage_markdown(
178
+ config: configuration,
179
+ base_url: base_url
180
+ )
171
181
  end
172
182
  end
173
183
 
174
184
  def llms_full
175
- render_llms('llms-full.txt') do
176
- Trifle::Docs::Helper::Llms.full_markdown(config: configuration)
185
+ base_url = llms_scope
186
+ llms_url = [base_url, 'llms-full.txt'].reject(&:empty?).join('/')
187
+ render_llms(llms_url) do
188
+ Trifle::Docs::Helper::Llms.full_markdown(
189
+ config: configuration,
190
+ base_url: base_url
191
+ )
177
192
  end
178
193
  end
179
194
 
180
195
  def sitemap
196
+ base_url = configuration.sitemap_base_url || request.base_url
181
197
  render_sitemap('sitemap.xml') do
182
- Trifle::Docs::Helper::Sitemap.xml(config: configuration)
198
+ Trifle::Docs::Helper::Sitemap.xml(config: configuration, base_url: base_url)
183
199
  end
184
200
  end
185
201
  end
@@ -6,32 +6,56 @@ module Trifle
6
6
  module Llms
7
7
  module_function
8
8
 
9
- def homepage_markdown(config: nil)
10
- meta = Trifle::Docs.meta(url: '', config: config)
9
+ def homepage_markdown(config: nil, base_url: '')
10
+ base_url = normalize_base_url(base_url)
11
+ meta = Trifle::Docs.meta(url: base_url, config: config)
11
12
  return nil if meta.nil?
12
13
 
13
14
  Trifle::Docs::Helper::MarkdownLayout.render(
14
15
  meta: meta,
15
- raw_content: Trifle::Docs.raw_content(url: '', config: config),
16
- sitemap: Trifle::Docs.sitemap(config: config)
16
+ raw_content: Trifle::Docs.raw_content(url: base_url, config: config)
17
17
  )
18
18
  end
19
19
 
20
- def full_markdown(config: nil)
20
+ def full_markdown(config: nil, base_url: '')
21
+ pages = llms_pages(config: config, base_url: base_url)
22
+ return nil if pages.nil?
23
+
24
+ pages.filter_map { |page| render_llms_page(page, config: config) }
25
+ .join("\n\n")
26
+ end
27
+
28
+ def llms_pages(config:, base_url:)
29
+ base_url = normalize_base_url(base_url)
21
30
  sitemap = Trifle::Docs.sitemap(config: config)
22
- pages = flatten_sitemap(sitemap)
31
+ scoped_sitemap = sitemap_subtree(sitemap, base_url)
32
+ return nil if scoped_sitemap.nil?
23
33
 
24
- chunks = pages.filter_map do |page|
25
- meta = page[:meta]
26
- next if meta.nil? || meta['type'] == 'file'
34
+ path = base_url.empty? ? [] : base_url.split('/')
35
+ flatten_sitemap(scoped_sitemap, path)
36
+ end
27
37
 
28
- raw = Trifle::Docs.raw_content(url: page[:url], config: config)
29
- next if raw.nil? || raw.strip.empty?
38
+ def render_llms_page(page, config:)
39
+ meta = page[:meta]
40
+ return nil if meta.nil? || meta['type'] == 'file'
30
41
 
31
- format_page(meta: meta, url: page[:url], raw_content: raw)
32
- end
42
+ raw = Trifle::Docs.raw_content(url: page[:url], config: config)
43
+ return nil if raw.nil? || raw.strip.empty?
44
+
45
+ format_page(meta: meta, url: page[:url], raw_content: raw)
46
+ end
47
+
48
+ def sitemap_subtree(sitemap, base_url)
49
+ return nil unless sitemap.is_a?(Hash)
50
+
51
+ base_url = normalize_base_url(base_url)
52
+ return sitemap if base_url.empty?
53
+
54
+ sitemap.dig(*base_url.split('/'))
55
+ end
33
56
 
34
- chunks.join("\n\n")
57
+ def normalize_base_url(base_url)
58
+ base_url.to_s.gsub(%r{^/+}, '').gsub(%r{/+$}, '')
35
59
  end
36
60
 
37
61
  def flatten_sitemap(node, path = [])
@@ -6,15 +6,12 @@ module Trifle
6
6
  module MarkdownLayout
7
7
  module_function
8
8
 
9
- def render(meta:, raw_content:, sitemap:) # rubocop:disable Metrics/MethodLength
9
+ def render(meta:, raw_content:)
10
10
  lines = []
11
11
  title = meta['title'] || derive_title_from_url(meta['url'])
12
12
 
13
13
  lines << "# #{title}"
14
14
  lines << ''
15
- lines << '## Navigation'
16
- lines << navigation_toc(sitemap)
17
- lines << ''
18
15
  lines << '## Content'
19
16
  lines << raw_content.to_s.strip
20
17
  lines << ''
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'cgi'
4
4
  require 'time'
5
+ require 'uri'
5
6
 
6
7
  module Trifle
7
8
  module Docs
@@ -9,9 +10,10 @@ module Trifle
9
10
  module Sitemap
10
11
  module_function
11
12
 
12
- def xml(config: nil)
13
+ def xml(config: nil, base_url: nil)
13
14
  sitemap = Trifle::Docs.sitemap(config: config)
14
- urls = sitemap_urls(sitemap)
15
+ resolved_base_url = resolve_base_url(config: config, base_url: base_url)
16
+ urls = sitemap_urls(sitemap, base_url: resolved_base_url)
15
17
  return nil if urls.empty?
16
18
 
17
19
  build_document(urls)
@@ -31,13 +33,13 @@ module Trifle
31
33
  entries
32
34
  end
33
35
 
34
- def sitemap_urls(sitemap)
36
+ def sitemap_urls(sitemap, base_url: nil)
35
37
  entries = flatten_sitemap(sitemap)
36
38
  entries.filter_map do |entry|
37
39
  meta = entry[:meta]
38
40
  next if meta.nil? || meta['type'] == 'file'
39
41
 
40
- build_url_entry(entry[:url], meta)
42
+ build_url_entry(entry[:url], meta, base_url: base_url)
41
43
  end
42
44
  end
43
45
 
@@ -50,21 +52,59 @@ module Trifle
50
52
  ].join("\n")
51
53
  end
52
54
 
53
- def build_url_entry(url, meta)
54
- loc = normalize_loc(url, meta)
55
+ def build_url_entry(url, meta, base_url: nil)
56
+ loc = normalize_loc(url, meta, base_url: base_url)
55
57
  lastmod = format_lastmod(meta['updated_at'])
56
58
  lastmod_tag = lastmod ? "<lastmod>#{lastmod}</lastmod>" : ''
57
59
 
58
60
  "<url><loc>#{loc}</loc>#{lastmod_tag}</url>"
59
61
  end
60
62
 
61
- def normalize_loc(url, meta)
63
+ def normalize_loc(url, meta, base_url: nil)
62
64
  loc = meta['url']
63
65
  loc = "/#{url}" if loc.nil? || loc.empty?
64
66
  loc = '/' if loc == '//'
67
+ loc = absolutize_loc(loc, base_url)
65
68
  CGI.escapeHTML(loc)
66
69
  end
67
70
 
71
+ def absolutize_loc(loc, base_url)
72
+ return loc if base_url.nil? || absolute_url?(loc)
73
+
74
+ path = loc.to_s.gsub(%r{^/+}, '')
75
+ return "#{base_url}/" if path.empty?
76
+
77
+ "#{base_url}/#{path}"
78
+ end
79
+
80
+ def absolute_url?(loc)
81
+ loc.to_s.match?(%r{\Ahttps?://}i)
82
+ end
83
+
84
+ def resolve_base_url(config:, base_url:)
85
+ normalize_base_url(base_url) || normalize_base_url(configuration_base_url(config))
86
+ end
87
+
88
+ def configuration_base_url(config)
89
+ return nil unless config.respond_to?(:sitemap_base_url)
90
+
91
+ config.sitemap_base_url
92
+ end
93
+
94
+ def normalize_base_url(base_url)
95
+ value = base_url.to_s.strip
96
+ return nil if value.empty?
97
+
98
+ uri = URI.parse(value)
99
+ return nil unless uri.is_a?(URI::HTTP) && uri.host
100
+
101
+ uri.query = nil
102
+ uri.fragment = nil
103
+ uri.to_s.gsub(%r{/+$}, '')
104
+ rescue URI::InvalidURIError
105
+ nil
106
+ end
107
+
68
108
  def format_lastmod(value)
69
109
  return nil if value.nil?
70
110
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Trifle
4
4
  module Docs
5
- VERSION = '0.7.1'
5
+ VERSION = '0.7.2'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trifle-docs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.1
4
+ version: 0.7.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jozef Vaclavik
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-01-28 00:00:00.000000000 Z
11
+ date: 2026-02-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler