aris 1.4.2 → 1.5.0
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 +4 -4
- data/CHANGELOG.md +251 -0
- data/README.md +18 -0
- data/docs/ADAPTERS.md +478 -0
- data/docs/ARCHITECTURE.md +222 -0
- data/docs/CONTENT.md +967 -0
- data/docs/PERFORMANCE.md +492 -0
- data/docs/PLUGIN_DEVELOPMENT.md +688 -0
- data/docs/USAGE.md +4998 -0
- data/docs/plugins/API_KEY_AUTH.md +232 -0
- data/docs/plugins/BASIC_AUTH.md +582 -0
- data/docs/plugins/BEARER_AUTH.md +394 -0
- data/docs/plugins/CACHE.md +369 -0
- data/docs/plugins/COMPRESSION.md +216 -0
- data/docs/plugins/COOKIES.md +30 -0
- data/docs/plugins/CORS.md +283 -0
- data/docs/plugins/CSRF.md +751 -0
- data/docs/plugins/ETAG.md +308 -0
- data/docs/plugins/FORM_PARSER.md +193 -0
- data/docs/plugins/HEALTH_CHECK.md +469 -0
- data/docs/plugins/JSON.md +291 -0
- data/docs/plugins/MULTIPART.md +427 -0
- data/docs/plugins/RATE_LIMITER.md +368 -0
- data/docs/plugins/REQUEST_ID.md +369 -0
- data/docs/plugins/REQUEST_LOGGER.md +151 -0
- data/docs/plugins/SECURITY.md +193 -0
- data/docs/plugins/SESSION.md +98 -0
- data/lib/aris/adapters/rack/adapter.rb +17 -2
- data/lib/aris/adapters/rack/request.rb +29 -11
- data/lib/aris/plugins/basic_auth.rb +3 -1
- data/lib/aris/plugins/cookies.rb +4 -32
- data/lib/aris/plugins/cors.rb +8 -1
- data/lib/aris/plugins/csrf.rb +63 -22
- data/lib/aris/plugins/flash.rb +3 -1
- data/lib/aris/plugins/form_parser.rb +52 -31
- data/lib/aris/plugins/multipart.rb +22 -2
- data/lib/aris/plugins/request_logger.rb +8 -1
- data/lib/aris/plugins/security_headers.rb +8 -1
- data/lib/aris/plugins/session.rb +150 -99
- data/lib/aris/response_helpers.rb +41 -0
- data/lib/aris/version.rb +2 -2
- metadata +31 -3
data/docs/CONTENT.md
ADDED
|
@@ -0,0 +1,967 @@
|
|
|
1
|
+
# Aris Content - Complete Specification
|
|
2
|
+
|
|
3
|
+
NOTE!: This is yet to be implemented. Features are currently onin flux.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
**aris-content** is a companion gem for **aris** that provides content intelligence and SEO utilities for multi-domain, static-first Ruby sites. It operates in two silos:
|
|
8
|
+
|
|
9
|
+
1. **Hub** - Development-time feedback, metrics, and insights
|
|
10
|
+
2. **Generators** - Production-ready SEO utilities (sitemap, robots, meta, schema)
|
|
11
|
+
|
|
12
|
+
**Philosophy:**
|
|
13
|
+
- Declarative at the handler level
|
|
14
|
+
- Zero performance impact on production requests
|
|
15
|
+
- Works with both file-based discovery and hash-based routes
|
|
16
|
+
- Unix philosophy: optional, composable, focused
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
# Gemfile
|
|
24
|
+
gem 'aris' # Core router
|
|
25
|
+
gem 'aris-content' # Content intelligence
|
|
26
|
+
|
|
27
|
+
# Auto-loads and extends Aris with content features
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## SILO 1: Hub (Development Intelligence)
|
|
33
|
+
|
|
34
|
+
### Architecture
|
|
35
|
+
|
|
36
|
+
- SQLite database (`.aris/content.db`) - gitignored
|
|
37
|
+
- Git integration for freshness tracking
|
|
38
|
+
- File watcher for live updates
|
|
39
|
+
- Separate Rack server for dashboard
|
|
40
|
+
- Zero impact on main app performance
|
|
41
|
+
|
|
42
|
+
### Features
|
|
43
|
+
|
|
44
|
+
#### 1. Content Indexer
|
|
45
|
+
Scans all routes (discovered + hash-based) and extracts:
|
|
46
|
+
- Domain, path, method
|
|
47
|
+
- Handler metadata (meta, sitemap, schema, redirects)
|
|
48
|
+
- Content analysis (word count, keywords, headings)
|
|
49
|
+
- Performance metrics (HTML size, image count)
|
|
50
|
+
- Link structure (internal/external)
|
|
51
|
+
|
|
52
|
+
#### 2. Health Score
|
|
53
|
+
Aggregate score (0-100) based on:
|
|
54
|
+
- Meta tags present
|
|
55
|
+
- Content freshness
|
|
56
|
+
- Performance budgets met
|
|
57
|
+
- Link graph health
|
|
58
|
+
- Image optimization
|
|
59
|
+
- Heading structure
|
|
60
|
+
|
|
61
|
+
#### 3. Freshness Tracker
|
|
62
|
+
Uses Git to track:
|
|
63
|
+
- Last updated timestamp
|
|
64
|
+
- Author of last change
|
|
65
|
+
- Commit history
|
|
66
|
+
- Time since last update
|
|
67
|
+
- Stale content warnings (configurable threshold)
|
|
68
|
+
|
|
69
|
+
#### 4. Performance Budgets
|
|
70
|
+
Tracks and warns:
|
|
71
|
+
- HTML size
|
|
72
|
+
- Total page weight
|
|
73
|
+
- Image count/size
|
|
74
|
+
- External requests
|
|
75
|
+
- Custom metrics
|
|
76
|
+
|
|
77
|
+
#### 5. Link Graph Analysis
|
|
78
|
+
- Internal link structure
|
|
79
|
+
- Orphaned pages (no incoming links)
|
|
80
|
+
- Deep pages (5+ clicks from home)
|
|
81
|
+
- Broken internal links
|
|
82
|
+
- Most/least linked pages
|
|
83
|
+
- Link graph visualization
|
|
84
|
+
|
|
85
|
+
#### 6. SEO Validation
|
|
86
|
+
- Missing meta tags
|
|
87
|
+
- Duplicate titles/descriptions
|
|
88
|
+
- Heading hierarchy issues (H1/H2/H3)
|
|
89
|
+
- Image alt text missing
|
|
90
|
+
- Canonical issues
|
|
91
|
+
- Schema.org validation
|
|
92
|
+
|
|
93
|
+
#### 7. Content Analysis
|
|
94
|
+
- Word count
|
|
95
|
+
- Reading time
|
|
96
|
+
- Keyword extraction & frequency
|
|
97
|
+
- Top keywords per page
|
|
98
|
+
- Content comparison tool
|
|
99
|
+
|
|
100
|
+
#### 8. Dashboard Server
|
|
101
|
+
- Clean HTML interface (Alpine.js for interactivity)
|
|
102
|
+
- Issue list (critical/warning/info)
|
|
103
|
+
- Per-page detail views
|
|
104
|
+
- Quick wins recommendations
|
|
105
|
+
- Auto-refresh on file changes
|
|
106
|
+
- Export to JSON
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
### Hub CLI Commands
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Start dashboard server
|
|
114
|
+
$ aris content hub
|
|
115
|
+
🚀 Content Hub starting...
|
|
116
|
+
📊 Dashboard: http://localhost:4000
|
|
117
|
+
🔍 Analyzing 247 pages across 3 domains...
|
|
118
|
+
✅ Ready in 2.1s
|
|
119
|
+
|
|
120
|
+
# Quick status check
|
|
121
|
+
$ aris content status
|
|
122
|
+
Health Score: 87/100
|
|
123
|
+
Critical Issues: 4
|
|
124
|
+
Warnings: 12
|
|
125
|
+
Pages: 247
|
|
126
|
+
|
|
127
|
+
# List stale content
|
|
128
|
+
$ aris content stale
|
|
129
|
+
/blog/old-post (18 months ago)
|
|
130
|
+
/about/team (14 months ago)
|
|
131
|
+
/services/legacy (9 months ago)
|
|
132
|
+
|
|
133
|
+
# Find orphaned pages
|
|
134
|
+
$ aris content orphans
|
|
135
|
+
/hidden/page (0 incoming links)
|
|
136
|
+
/test/sandbox (0 incoming links)
|
|
137
|
+
|
|
138
|
+
# Performance budget check
|
|
139
|
+
$ aris content budgets
|
|
140
|
+
❌ /blog/post-1 (89kb > 50kb budget)
|
|
141
|
+
⚠️ /products (52kb > 50kb budget)
|
|
142
|
+
✅ 245 pages within budget
|
|
143
|
+
|
|
144
|
+
# Export data
|
|
145
|
+
$ aris content export --format json > content-report.json
|
|
146
|
+
|
|
147
|
+
# Compare two pages
|
|
148
|
+
$ aris content compare /blog/post-1 /blog/post-2
|
|
149
|
+
Word count: 1,234 vs 456
|
|
150
|
+
Keywords: ruby(12) vs rails(8)
|
|
151
|
+
Links out: 5 vs 2
|
|
152
|
+
Images: 3 vs 1
|
|
153
|
+
Read time: 6min vs 2min
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### Hub Configuration
|
|
159
|
+
|
|
160
|
+
```ruby
|
|
161
|
+
# config/aris_content.rb
|
|
162
|
+
Aris::Content.configure do |config|
|
|
163
|
+
# Performance budgets
|
|
164
|
+
config.performance_budget do |budget|
|
|
165
|
+
budget.max_html_size = 50.kilobytes
|
|
166
|
+
budget.max_images = 10
|
|
167
|
+
budget.max_external_requests = 20
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Freshness thresholds
|
|
171
|
+
config.freshness_threshold = 6.months
|
|
172
|
+
config.freshness_warning = 3.months
|
|
173
|
+
|
|
174
|
+
# Target keywords per domain
|
|
175
|
+
config.target_keywords "example.com", %w[ruby router performance seo]
|
|
176
|
+
config.target_keywords "shop.example.com", %w[products ecommerce checkout]
|
|
177
|
+
|
|
178
|
+
# Dashboard settings
|
|
179
|
+
config.dashboard_port = 4000
|
|
180
|
+
config.dashboard_refresh_interval = 5.seconds
|
|
181
|
+
|
|
182
|
+
# Exclude paths from analysis
|
|
183
|
+
config.exclude_paths %w[/admin /api/internal /test]
|
|
184
|
+
end
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
## SILO 2: Generators (SEO Utilities)
|
|
190
|
+
|
|
191
|
+
### 1. Sitemap Generator
|
|
192
|
+
|
|
193
|
+
#### Handler DSL
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# File-based route
|
|
197
|
+
module Handler
|
|
198
|
+
extend Aris::RouteHelpers
|
|
199
|
+
|
|
200
|
+
sitemap(
|
|
201
|
+
priority: 0.8,
|
|
202
|
+
changefreq: 'weekly',
|
|
203
|
+
lastmod: '2025-01-15'
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
def self.call(request, params)
|
|
207
|
+
# ...
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
#### Dynamic Sitemap (Multiple URLs per Route)
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
sitemap do
|
|
216
|
+
# Return array of URL data
|
|
217
|
+
Post.all.map do |post|
|
|
218
|
+
{
|
|
219
|
+
path: "/blog/#{post.slug}",
|
|
220
|
+
priority: post.featured? ? 1.0 : 0.7,
|
|
221
|
+
changefreq: 'daily',
|
|
222
|
+
lastmod: post.updated_at
|
|
223
|
+
}
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### Hash-based Routes
|
|
229
|
+
|
|
230
|
+
```ruby
|
|
231
|
+
Aris.routes({
|
|
232
|
+
"example.com": {
|
|
233
|
+
"/": {
|
|
234
|
+
get: {
|
|
235
|
+
to: HomeHandler,
|
|
236
|
+
sitemap: { priority: 1.0, changefreq: 'daily' }
|
|
237
|
+
}
|
|
238
|
+
},
|
|
239
|
+
"/about": {
|
|
240
|
+
get: {
|
|
241
|
+
to: AboutHandler,
|
|
242
|
+
sitemap: { priority: 0.8, changefreq: 'monthly' }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
})
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Auto-serves `/sitemap.xml`
|
|
250
|
+
|
|
251
|
+
```xml
|
|
252
|
+
GET /sitemap.xml
|
|
253
|
+
|
|
254
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
255
|
+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
256
|
+
<url>
|
|
257
|
+
<loc>https://example.com/</loc>
|
|
258
|
+
<lastmod>2025-01-15</lastmod>
|
|
259
|
+
<changefreq>daily</changefreq>
|
|
260
|
+
<priority>1.0</priority>
|
|
261
|
+
</url>
|
|
262
|
+
<url>
|
|
263
|
+
<loc>https://example.com/about</loc>
|
|
264
|
+
<changefreq>monthly</changefreq>
|
|
265
|
+
<priority>0.8</priority>
|
|
266
|
+
</url>
|
|
267
|
+
</urlset>
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
### 2. Robots.txt Generator
|
|
273
|
+
|
|
274
|
+
#### Handler DSL
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
robots(
|
|
278
|
+
allow: ["/", "/blog/*"],
|
|
279
|
+
disallow: ["/admin", "/api/*", "/private"],
|
|
280
|
+
crawl_delay: 1
|
|
281
|
+
)
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
#### Domain-level Configuration
|
|
285
|
+
|
|
286
|
+
```ruby
|
|
287
|
+
Aris::Content::Robots.configure do |config|
|
|
288
|
+
config.domain "example.com" do
|
|
289
|
+
allow ["/", "/blog/*", "/products/*"]
|
|
290
|
+
disallow ["/admin", "/private"]
|
|
291
|
+
sitemap "https://example.com/sitemap.xml"
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
config.domain "staging.example.com" do
|
|
295
|
+
disallow ["/"] # Block all on staging
|
|
296
|
+
end
|
|
297
|
+
end
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
#### Auto-serves `/robots.txt`
|
|
301
|
+
|
|
302
|
+
```
|
|
303
|
+
GET /robots.txt
|
|
304
|
+
|
|
305
|
+
User-agent: *
|
|
306
|
+
Allow: /
|
|
307
|
+
Allow: /blog/*
|
|
308
|
+
Disallow: /admin
|
|
309
|
+
Disallow: /api/*
|
|
310
|
+
Crawl-delay: 1
|
|
311
|
+
|
|
312
|
+
Sitemap: https://example.com/sitemap.xml
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### 3. Meta Tags Manager
|
|
318
|
+
|
|
319
|
+
#### Handler DSL
|
|
320
|
+
|
|
321
|
+
```ruby
|
|
322
|
+
meta(
|
|
323
|
+
title: "Best Ruby Framework 2025",
|
|
324
|
+
description: "Deploy sites across multiple domains with lightning speed",
|
|
325
|
+
keywords: %w[ruby router framework multi-domain],
|
|
326
|
+
|
|
327
|
+
# Open Graph
|
|
328
|
+
og_title: "Custom OG Title",
|
|
329
|
+
og_description: "Custom OG Description",
|
|
330
|
+
og_image: "/images/og-hero.jpg",
|
|
331
|
+
og_type: "website",
|
|
332
|
+
|
|
333
|
+
# Twitter Card
|
|
334
|
+
twitter_card: "summary_large_image",
|
|
335
|
+
twitter_site: "@example",
|
|
336
|
+
twitter_creator: "@author",
|
|
337
|
+
|
|
338
|
+
# SEO
|
|
339
|
+
canonical: true, # Auto-generates from current path
|
|
340
|
+
robots: "index, follow"
|
|
341
|
+
)
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
#### Dynamic Meta
|
|
345
|
+
|
|
346
|
+
```ruby
|
|
347
|
+
meta do
|
|
348
|
+
{
|
|
349
|
+
title: "#{@product.name} | Example Store",
|
|
350
|
+
description: @product.description.truncate(160),
|
|
351
|
+
og_image: @product.image_url,
|
|
352
|
+
og_type: "product"
|
|
353
|
+
}
|
|
354
|
+
end
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
#### Hash-based Routes
|
|
358
|
+
|
|
359
|
+
```ruby
|
|
360
|
+
Aris.routes({
|
|
361
|
+
"example.com": {
|
|
362
|
+
"/": {
|
|
363
|
+
get: {
|
|
364
|
+
to: HomeHandler,
|
|
365
|
+
meta: {
|
|
366
|
+
title: "Welcome Home",
|
|
367
|
+
description: "...",
|
|
368
|
+
og_image: "/hero.jpg"
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
#### Rendering in Layout
|
|
377
|
+
|
|
378
|
+
```erb
|
|
379
|
+
<!-- layout.html.erb -->
|
|
380
|
+
<head>
|
|
381
|
+
<%= Aris::Content::Meta.render %>
|
|
382
|
+
</head>
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**Outputs:**
|
|
386
|
+
|
|
387
|
+
```html
|
|
388
|
+
<title>Best Ruby Framework 2025</title>
|
|
389
|
+
<meta name="description" content="Deploy sites across multiple domains...">
|
|
390
|
+
<meta name="keywords" content="ruby, router, framework, multi-domain">
|
|
391
|
+
|
|
392
|
+
<meta property="og:title" content="Custom OG Title">
|
|
393
|
+
<meta property="og:description" content="Custom OG Description">
|
|
394
|
+
<meta property="og:image" content="https://example.com/images/og-hero.jpg">
|
|
395
|
+
<meta property="og:type" content="website">
|
|
396
|
+
<meta property="og:url" content="https://example.com/current-page">
|
|
397
|
+
|
|
398
|
+
<meta name="twitter:card" content="summary_large_image">
|
|
399
|
+
<meta name="twitter:site" content="@example">
|
|
400
|
+
<meta name="twitter:creator" content="@author">
|
|
401
|
+
|
|
402
|
+
<link rel="canonical" href="https://example.com/current-page">
|
|
403
|
+
<meta name="robots" content="index, follow">
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
### 4. Structured Data (Schema.org)
|
|
409
|
+
|
|
410
|
+
#### Handler DSL
|
|
411
|
+
|
|
412
|
+
```ruby
|
|
413
|
+
schema(
|
|
414
|
+
type: "Article",
|
|
415
|
+
headline: "My Amazing Blog Post",
|
|
416
|
+
author: {
|
|
417
|
+
"@type": "Person",
|
|
418
|
+
name: "John Doe"
|
|
419
|
+
},
|
|
420
|
+
datePublished: "2025-01-15",
|
|
421
|
+
dateModified: "2025-01-20",
|
|
422
|
+
image: "/images/post-hero.jpg",
|
|
423
|
+
publisher: {
|
|
424
|
+
"@type": "Organization",
|
|
425
|
+
name: "Example Company",
|
|
426
|
+
logo: "/logo.png"
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
#### Dynamic Schema
|
|
432
|
+
|
|
433
|
+
```ruby
|
|
434
|
+
schema do
|
|
435
|
+
{
|
|
436
|
+
"@type": "Product",
|
|
437
|
+
name: @product.name,
|
|
438
|
+
description: @product.description,
|
|
439
|
+
image: @product.image_url,
|
|
440
|
+
offers: {
|
|
441
|
+
"@type": "Offer",
|
|
442
|
+
price: @product.price,
|
|
443
|
+
priceCurrency: "USD",
|
|
444
|
+
availability: @product.in_stock? ? "InStock" : "OutOfStock"
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
end
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
#### Built-in Templates
|
|
451
|
+
|
|
452
|
+
```ruby
|
|
453
|
+
schema :article do |s|
|
|
454
|
+
s.headline = "My Post"
|
|
455
|
+
s.author = "John Doe"
|
|
456
|
+
s.date_published = "2025-01-15"
|
|
457
|
+
end
|
|
458
|
+
|
|
459
|
+
schema :product do |s|
|
|
460
|
+
s.name = @product.name
|
|
461
|
+
s.price = @product.price
|
|
462
|
+
s.image = @product.image_url
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
schema :organization do |s|
|
|
466
|
+
s.name = "Example Company"
|
|
467
|
+
s.url = "https://example.com"
|
|
468
|
+
s.logo = "/logo.png"
|
|
469
|
+
s.social_profiles = [
|
|
470
|
+
"https://twitter.com/example",
|
|
471
|
+
"https://facebook.com/example"
|
|
472
|
+
]
|
|
473
|
+
end
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
#### Rendering in Layout
|
|
477
|
+
|
|
478
|
+
```erb
|
|
479
|
+
<head>
|
|
480
|
+
<%= Aris::Content::Schema.render %>
|
|
481
|
+
</head>
|
|
482
|
+
```
|
|
483
|
+
|
|
484
|
+
**Outputs:**
|
|
485
|
+
|
|
486
|
+
```html
|
|
487
|
+
<script type="application/ld+json">
|
|
488
|
+
{
|
|
489
|
+
"@context": "https://schema.org",
|
|
490
|
+
"@type": "Article",
|
|
491
|
+
"headline": "My Amazing Blog Post",
|
|
492
|
+
"author": {
|
|
493
|
+
"@type": "Person",
|
|
494
|
+
"name": "John Doe"
|
|
495
|
+
},
|
|
496
|
+
"datePublished": "2025-01-15",
|
|
497
|
+
"dateModified": "2025-01-20",
|
|
498
|
+
"image": "https://example.com/images/post-hero.jpg",
|
|
499
|
+
"publisher": {
|
|
500
|
+
"@type": "Organization",
|
|
501
|
+
"name": "Example Company",
|
|
502
|
+
"logo": "https://example.com/logo.png"
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
</script>
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
---
|
|
509
|
+
|
|
510
|
+
### 5. RSS/Atom Feed Generator
|
|
511
|
+
|
|
512
|
+
#### Configuration
|
|
513
|
+
|
|
514
|
+
```ruby
|
|
515
|
+
Aris::Content::Feed.configure do |config|
|
|
516
|
+
config.domain "example.com" do
|
|
517
|
+
title "Example Blog"
|
|
518
|
+
description "Latest posts from Example"
|
|
519
|
+
link "https://example.com"
|
|
520
|
+
|
|
521
|
+
items do
|
|
522
|
+
Post.recent(20).map do |post|
|
|
523
|
+
{
|
|
524
|
+
title: post.title,
|
|
525
|
+
link: "https://example.com/blog/#{post.slug}",
|
|
526
|
+
description: post.excerpt,
|
|
527
|
+
pubDate: post.published_at,
|
|
528
|
+
guid: post.id
|
|
529
|
+
}
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
end
|
|
533
|
+
end
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
#### Auto-serves `/feed.xml`
|
|
537
|
+
|
|
538
|
+
```xml
|
|
539
|
+
GET /feed.xml
|
|
540
|
+
|
|
541
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
542
|
+
<rss version="2.0">
|
|
543
|
+
<channel>
|
|
544
|
+
<title>Example Blog</title>
|
|
545
|
+
<link>https://example.com</link>
|
|
546
|
+
<description>Latest posts from Example</description>
|
|
547
|
+
<item>
|
|
548
|
+
<title>My Blog Post</title>
|
|
549
|
+
<link>https://example.com/blog/my-post</link>
|
|
550
|
+
<description>Post excerpt...</description>
|
|
551
|
+
<pubDate>Mon, 15 Jan 2025 10:00:00 GMT</pubDate>
|
|
552
|
+
<guid>123</guid>
|
|
553
|
+
</item>
|
|
554
|
+
</channel>
|
|
555
|
+
</rss>
|
|
556
|
+
```
|
|
557
|
+
|
|
558
|
+
#### Auto-injects Feed Link
|
|
559
|
+
|
|
560
|
+
```html
|
|
561
|
+
<head>
|
|
562
|
+
<link rel="alternate" type="application/rss+xml"
|
|
563
|
+
title="Example Blog"
|
|
564
|
+
href="https://example.com/feed.xml">
|
|
565
|
+
</head>
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
---
|
|
569
|
+
|
|
570
|
+
### 6. Breadcrumbs Generator
|
|
571
|
+
|
|
572
|
+
#### Handler DSL
|
|
573
|
+
|
|
574
|
+
```ruby
|
|
575
|
+
breadcrumbs auto: true # Generates from route structure
|
|
576
|
+
|
|
577
|
+
# Or custom
|
|
578
|
+
breadcrumbs [
|
|
579
|
+
{ name: "Home", url: "/" },
|
|
580
|
+
{ name: "Blog", url: "/blog" },
|
|
581
|
+
{ name: "My Post" } # Current page, no URL
|
|
582
|
+
]
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
#### Rendering
|
|
586
|
+
|
|
587
|
+
```erb
|
|
588
|
+
<%= Aris::Content::Breadcrumbs.render %>
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**Outputs:**
|
|
592
|
+
|
|
593
|
+
```html
|
|
594
|
+
<nav aria-label="breadcrumb">
|
|
595
|
+
<ol itemscope itemtype="https://schema.org/BreadcrumbList">
|
|
596
|
+
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
597
|
+
<a itemprop="item" href="/">
|
|
598
|
+
<span itemprop="name">Home</span>
|
|
599
|
+
</a>
|
|
600
|
+
<meta itemprop="position" content="1">
|
|
601
|
+
</li>
|
|
602
|
+
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
603
|
+
<a itemprop="item" href="/blog">
|
|
604
|
+
<span itemprop="name">Blog</span>
|
|
605
|
+
</a>
|
|
606
|
+
<meta itemprop="position" content="2">
|
|
607
|
+
</li>
|
|
608
|
+
<li itemprop="itemListElement" itemscope itemtype="https://schema.org/ListItem">
|
|
609
|
+
<span itemprop="name">My Post</span>
|
|
610
|
+
<meta itemprop="position" content="3">
|
|
611
|
+
</li>
|
|
612
|
+
</ol>
|
|
613
|
+
</nav>
|
|
614
|
+
|
|
615
|
+
<script type="application/ld+json">
|
|
616
|
+
{
|
|
617
|
+
"@context": "https://schema.org",
|
|
618
|
+
"@type": "BreadcrumbList",
|
|
619
|
+
"itemListElement": [...]
|
|
620
|
+
}
|
|
621
|
+
</script>
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
---
|
|
625
|
+
|
|
626
|
+
## Integration with Aris Core
|
|
627
|
+
|
|
628
|
+
### Automatic Hook Points
|
|
629
|
+
|
|
630
|
+
```ruby
|
|
631
|
+
# In aris-content, automatically extends:
|
|
632
|
+
|
|
633
|
+
# 1. RouteHelpers module
|
|
634
|
+
module Aris::RouteHelpers
|
|
635
|
+
# Adds: sitemap, meta, schema, robots, breadcrumbs methods
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
# 2. Discovery system
|
|
639
|
+
module Aris::Discovery
|
|
640
|
+
# Hooks into add_route_to_hash to register metadata
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
# 3. Compiler
|
|
644
|
+
module Aris
|
|
645
|
+
# Hooks into routes() to extract metadata from hash routes
|
|
646
|
+
end
|
|
647
|
+
|
|
648
|
+
# 4. Base adapter
|
|
649
|
+
module Aris::Adapters::Base
|
|
650
|
+
# Adds: handle_sitemap, handle_robots, handle_feed methods
|
|
651
|
+
end
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Zero Configuration Required
|
|
655
|
+
|
|
656
|
+
```ruby
|
|
657
|
+
# Just require the gem
|
|
658
|
+
require 'aris/content'
|
|
659
|
+
|
|
660
|
+
# Everything works automatically:
|
|
661
|
+
# - Handlers can use sitemap/meta/schema
|
|
662
|
+
# - /sitemap.xml auto-serves
|
|
663
|
+
# - /robots.txt auto-serves
|
|
664
|
+
# - /feed.xml auto-serves
|
|
665
|
+
# - Hub commands available
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
---
|
|
669
|
+
|
|
670
|
+
## Production Metrics (Optional Plugin)
|
|
671
|
+
|
|
672
|
+
### Opt-in Tracking
|
|
673
|
+
|
|
674
|
+
```ruby
|
|
675
|
+
# Gemfile
|
|
676
|
+
gem 'aris-content', require: ['aris/content', 'aris/content/metrics']
|
|
677
|
+
|
|
678
|
+
# config.ru (production)
|
|
679
|
+
use Aris::Content::MetricsCollector,
|
|
680
|
+
storage: './metrics.db',
|
|
681
|
+
sample_rate: 0.1 # Track 10% of requests
|
|
682
|
+
|
|
683
|
+
run Aris::Adapters::RackApp.new
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
### What's Tracked (Minimal, Privacy-Focused)
|
|
687
|
+
|
|
688
|
+
- Path
|
|
689
|
+
- Domain
|
|
690
|
+
- Status code
|
|
691
|
+
- Response time
|
|
692
|
+
- Timestamp
|
|
693
|
+
- **NO user data, NO IP addresses, NO PII**
|
|
694
|
+
|
|
695
|
+
### Pull to Dev
|
|
696
|
+
|
|
697
|
+
```bash
|
|
698
|
+
# Export from production
|
|
699
|
+
$ aris content export-metrics > metrics.json
|
|
700
|
+
|
|
701
|
+
# Import to dev
|
|
702
|
+
$ aris content import-metrics metrics.json
|
|
703
|
+
|
|
704
|
+
# Dashboard now shows production data
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
**Hub Dashboard with Production Data:**
|
|
708
|
+
|
|
709
|
+
```
|
|
710
|
+
TOP PAGES (Last 30 Days)
|
|
711
|
+
|
|
712
|
+
Production Data: ✓
|
|
713
|
+
|
|
714
|
+
Path Views Avg Time Health
|
|
715
|
+
/ 45,231 120ms ✅ 95
|
|
716
|
+
/blog/popular-post 12,450 340ms ⚠️ 72
|
|
717
|
+
/products 8,932 890ms ❌ 45 (slow!)
|
|
718
|
+
```
|
|
719
|
+
|
|
720
|
+
---
|
|
721
|
+
|
|
722
|
+
## Developer Experience Examples
|
|
723
|
+
|
|
724
|
+
### Example 1: Blog Post with Full SEO
|
|
725
|
+
|
|
726
|
+
```ruby
|
|
727
|
+
# app/routes/example.com/blog/:slug/get.rb
|
|
728
|
+
module Handler
|
|
729
|
+
extend Aris::RouteHelpers
|
|
730
|
+
|
|
731
|
+
# Sitemap
|
|
732
|
+
sitemap do
|
|
733
|
+
Post.published.map do |post|
|
|
734
|
+
{
|
|
735
|
+
path: "/blog/#{post.slug}",
|
|
736
|
+
priority: post.featured? ? 1.0 : 0.7,
|
|
737
|
+
changefreq: 'weekly',
|
|
738
|
+
lastmod: post.updated_at
|
|
739
|
+
}
|
|
740
|
+
end
|
|
741
|
+
end
|
|
742
|
+
|
|
743
|
+
# Meta tags
|
|
744
|
+
meta do
|
|
745
|
+
{
|
|
746
|
+
title: "#{@post.title} | Example Blog",
|
|
747
|
+
description: @post.excerpt.truncate(160),
|
|
748
|
+
keywords: @post.tags,
|
|
749
|
+
og_image: @post.hero_image_url,
|
|
750
|
+
og_type: "article",
|
|
751
|
+
twitter_card: "summary_large_image"
|
|
752
|
+
}
|
|
753
|
+
end
|
|
754
|
+
|
|
755
|
+
# Structured data
|
|
756
|
+
schema :article do |s|
|
|
757
|
+
s.headline = @post.title
|
|
758
|
+
s.author = @post.author.name
|
|
759
|
+
s.date_published = @post.published_at
|
|
760
|
+
s.date_modified = @post.updated_at
|
|
761
|
+
s.image = @post.hero_image_url
|
|
762
|
+
end
|
|
763
|
+
|
|
764
|
+
# Breadcrumbs
|
|
765
|
+
breadcrumbs [
|
|
766
|
+
{ name: "Home", url: "/" },
|
|
767
|
+
{ name: "Blog", url: "/blog" },
|
|
768
|
+
{ name: @post.title }
|
|
769
|
+
]
|
|
770
|
+
|
|
771
|
+
def self.call(request, params)
|
|
772
|
+
@post = Post.find_by_slug(params[:slug])
|
|
773
|
+
render :blog_post
|
|
774
|
+
end
|
|
775
|
+
end
|
|
776
|
+
```
|
|
777
|
+
|
|
778
|
+
### Example 2: Product Page
|
|
779
|
+
|
|
780
|
+
```ruby
|
|
781
|
+
# app/routes/shop.example.com/products/:id/get.rb
|
|
782
|
+
module Handler
|
|
783
|
+
extend Aris::RouteHelpers
|
|
784
|
+
|
|
785
|
+
sitemap(priority: 0.8, changefreq: 'daily')
|
|
786
|
+
|
|
787
|
+
meta do
|
|
788
|
+
{
|
|
789
|
+
title: "#{@product.name} - $#{@product.price}",
|
|
790
|
+
description: @product.description,
|
|
791
|
+
og_type: "product",
|
|
792
|
+
og_image: @product.primary_image_url
|
|
793
|
+
}
|
|
794
|
+
end
|
|
795
|
+
|
|
796
|
+
schema :product do |s|
|
|
797
|
+
s.name = @product.name
|
|
798
|
+
s.description = @product.description
|
|
799
|
+
s.image = @product.images.map(&:url)
|
|
800
|
+
s.offers do
|
|
801
|
+
{
|
|
802
|
+
"@type": "Offer",
|
|
803
|
+
price: @product.price,
|
|
804
|
+
priceCurrency: "USD",
|
|
805
|
+
availability: @product.in_stock? ? "InStock" : "OutOfStock"
|
|
806
|
+
}
|
|
807
|
+
end
|
|
808
|
+
end
|
|
809
|
+
|
|
810
|
+
def self.call(request, params)
|
|
811
|
+
@product = Product.find(params[:id])
|
|
812
|
+
render :product
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
### Example 3: Simple Page (Hash Routes)
|
|
818
|
+
|
|
819
|
+
```ruby
|
|
820
|
+
Aris.routes({
|
|
821
|
+
"example.com": {
|
|
822
|
+
"/about": {
|
|
823
|
+
get: {
|
|
824
|
+
to: AboutHandler,
|
|
825
|
+
sitemap: { priority: 0.8, changefreq: 'monthly' },
|
|
826
|
+
meta: {
|
|
827
|
+
title: "About Us",
|
|
828
|
+
description: "Learn about our company",
|
|
829
|
+
og_image: "/images/about-hero.jpg"
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
})
|
|
835
|
+
```
|
|
836
|
+
|
|
837
|
+
---
|
|
838
|
+
|
|
839
|
+
## Dashboard Preview
|
|
840
|
+
|
|
841
|
+
```
|
|
842
|
+
╔════════════════════════════════════════════════════════╗
|
|
843
|
+
║ CONTENT HEALTH SCORE: 87/100 ║
|
|
844
|
+
║ Last updated: 3 seconds ago ║
|
|
845
|
+
╚════════════════════════════════════════════════════════╝
|
|
846
|
+
|
|
847
|
+
🔴 CRITICAL ISSUES (4)
|
|
848
|
+
|
|
849
|
+
/blog/old-post (example.com)
|
|
850
|
+
├─ Not updated in 18 months
|
|
851
|
+
├─ No internal links to this page
|
|
852
|
+
├─ Missing meta description
|
|
853
|
+
└─ [View Details] [Mark Reviewed]
|
|
854
|
+
|
|
855
|
+
/products/widget (shop.example.com)
|
|
856
|
+
├─ HTML size: 89kb (budget: 50kb)
|
|
857
|
+
├─ 3 images without alt text
|
|
858
|
+
├─ hero.jpg is 2.1MB (not optimized)
|
|
859
|
+
└─ [View Details] [Ignore]
|
|
860
|
+
|
|
861
|
+
⚠️ WARNINGS (12)
|
|
862
|
+
|
|
863
|
+
/about/team (example.com)
|
|
864
|
+
├─ Title is short (15 chars, recommend 50-60)
|
|
865
|
+
├─ Last updated 5 months ago
|
|
866
|
+
└─ [View Details]
|
|
867
|
+
|
|
868
|
+
✅ HEALTHY (231 pages)
|
|
869
|
+
|
|
870
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
871
|
+
|
|
872
|
+
QUICK WINS (Easy fixes, high impact)
|
|
873
|
+
|
|
874
|
+
1. Add meta descriptions (8 pages)
|
|
875
|
+
2. Optimize large images (12 images)
|
|
876
|
+
3. Fix duplicate titles (3 pages)
|
|
877
|
+
4. Link to orphaned pages (3 pages)
|
|
878
|
+
|
|
879
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
880
|
+
|
|
881
|
+
PERFORMANCE BUDGETS
|
|
882
|
+
|
|
883
|
+
example.com
|
|
884
|
+
├─ / ...................... ✅ 24kb (budget: 50kb)
|
|
885
|
+
├─ /about ................. ✅ 31kb (budget: 50kb)
|
|
886
|
+
├─ /blog/post-1 ........... ❌ 89kb (budget: 50kb)
|
|
887
|
+
└─ /contact ............... ✅ 18kb (budget: 50kb)
|
|
888
|
+
|
|
889
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
890
|
+
|
|
891
|
+
CONTENT FRESHNESS
|
|
892
|
+
|
|
893
|
+
🔴 STALE (6+ months) - 4 pages
|
|
894
|
+
🟡 AGING (3-6 months) - 12 pages
|
|
895
|
+
🟢 FRESH (<3 months) - 231 pages
|
|
896
|
+
|
|
897
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
898
|
+
|
|
899
|
+
LINK GRAPH
|
|
900
|
+
|
|
901
|
+
Orphaned Pages: 3
|
|
902
|
+
Deep Pages (5+ clicks): 7
|
|
903
|
+
Broken Links: 2
|
|
904
|
+
|
|
905
|
+
Most Linked:
|
|
906
|
+
1. /blog/ultimate-guide (47 links)
|
|
907
|
+
2. / (38 links)
|
|
908
|
+
3. /products (31 links)
|
|
909
|
+
|
|
910
|
+
[View Full Graph]
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
---
|
|
914
|
+
|
|
915
|
+
## Roadmap
|
|
916
|
+
|
|
917
|
+
### v0.1 (MVP)
|
|
918
|
+
- Content indexer
|
|
919
|
+
- Freshness tracker
|
|
920
|
+
- Performance budgets
|
|
921
|
+
- Link graph analysis
|
|
922
|
+
- Basic dashboard
|
|
923
|
+
- Sitemap generator
|
|
924
|
+
- robots.txt generator
|
|
925
|
+
- Meta tags manager
|
|
926
|
+
|
|
927
|
+
### v0.2
|
|
928
|
+
- Schema.org support
|
|
929
|
+
- Image analysis
|
|
930
|
+
- Heading validator
|
|
931
|
+
- RSS/Atom feeds
|
|
932
|
+
- Breadcrumbs
|
|
933
|
+
|
|
934
|
+
### v0.3
|
|
935
|
+
- Keyword extraction
|
|
936
|
+
- Content comparison
|
|
937
|
+
- Production metrics (opt-in)
|
|
938
|
+
- Dashboard improvements
|
|
939
|
+
|
|
940
|
+
### v1.0
|
|
941
|
+
- Content templates
|
|
942
|
+
- A/B test tracking
|
|
943
|
+
- Trend analysis
|
|
944
|
+
- Advanced visualizations
|
|
945
|
+
- Team features (maybe)
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## Summary
|
|
950
|
+
|
|
951
|
+
**aris-content** turns content management into a joy by:
|
|
952
|
+
|
|
953
|
+
1. **Showing you problems** before they hurt SEO
|
|
954
|
+
2. **Making SEO declarative** at the handler level
|
|
955
|
+
3. **Tracking freshness** automatically via Git
|
|
956
|
+
4. **Enforcing budgets** so pages stay fast
|
|
957
|
+
5. **Finding weak spots** in your content structure
|
|
958
|
+
6. **Generating everything** (sitemap, robots, meta, schema)
|
|
959
|
+
7. **Zero production cost** - it's all dev-time or generated
|
|
960
|
+
|
|
961
|
+
**The DX is chef's kiss:** Write `sitemap`, `meta`, `schema` in your handler, and everything just works. No separate config files, no manual XML editing, no third-party tools.
|
|
962
|
+
|
|
963
|
+
**This doesn't exist anywhere else.** Not in Rails, not in Sinatra, not in JS frameworks, not in PHP. This is your competitive advantage as a solo dev managing multiple domains.
|
|
964
|
+
|
|
965
|
+
---
|
|
966
|
+
|
|
967
|
+
Ready to build? 🚀
|