feat: Auto-detect text browsers and serve plain text CV
- Detect curl, wget, lynx, w3m, links, elinks, browsh, carbonyl - Check User-Agent and Accept: text/plain header - Redirect to /text endpoint automatically - Document in SEO guide and modern techniques
This commit is contained in:
@@ -163,6 +163,42 @@ description: Interactive curriculum vitae...
|
|||||||
|
|
||||||
**Purpose:** Provides AI systems (ChatGPT, Claude, Perplexity, etc.) with structured, human-readable information about the site content.
|
**Purpose:** Provides AI systems (ChatGPT, Claude, Perplexity, etc.) with structured, human-readable information about the site content.
|
||||||
|
|
||||||
|
#### Plain Text Auto-Detection (`/text` endpoint)
|
||||||
|
|
||||||
|
The site automatically detects text-based browsers and CLI tools, serving a clean 80-character plain text version:
|
||||||
|
|
||||||
|
**Auto-detected clients:**
|
||||||
|
| Client | Type |
|
||||||
|
|--------|------|
|
||||||
|
| curl | CLI tool |
|
||||||
|
| wget | CLI tool |
|
||||||
|
| HTTPie | CLI tool |
|
||||||
|
| Lynx | Text browser |
|
||||||
|
| w3m | Text browser |
|
||||||
|
| Links/ELinks | Text browser |
|
||||||
|
| Browsh | Terminal browser |
|
||||||
|
| Carbonyl | Terminal browser |
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# Auto-detected (serves plain text):
|
||||||
|
curl https://juan.andres.morenorub.io/
|
||||||
|
|
||||||
|
# Explicit endpoint:
|
||||||
|
curl https://juan.andres.morenorub.io/text?lang=en
|
||||||
|
|
||||||
|
# With Accept header:
|
||||||
|
curl -H "Accept: text/plain" https://juan.andres.morenorub.io/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Output features:**
|
||||||
|
- 80-character line wrapping
|
||||||
|
- ASCII art section headers
|
||||||
|
- Clean, structured text
|
||||||
|
- All CV content preserved
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
#### robots.txt AI Bot Rules (`static/robots.txt`)
|
#### robots.txt AI Bot Rules (`static/robots.txt`)
|
||||||
|
|
||||||
Explicit permissions for AI crawlers:
|
Explicit permissions for AI crawlers:
|
||||||
@@ -223,6 +259,8 @@ The implementation supports Google's E-E-A-T (Experience, Expertise, Authority,
|
|||||||
| `static/sitemap.xml` | XML sitemap for search engines |
|
| `static/sitemap.xml` | XML sitemap for search engines |
|
||||||
| `data/cv-en.json` | SEO fields (pageTitle, metaTitle, etc.) |
|
| `data/cv-en.json` | SEO fields (pageTitle, metaTitle, etc.) |
|
||||||
| `data/cv-es.json` | Spanish SEO fields |
|
| `data/cv-es.json` | Spanish SEO fields |
|
||||||
|
| `/text` endpoint | Plain text CV for CLI/TUI browsers |
|
||||||
|
| `templates/cv-text.txt` | Plain text template |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -3532,6 +3532,55 @@ Allow: /
|
|||||||
| **Authority** | Social links (LinkedIn, GitHub), company associations |
|
| **Authority** | Social links (LinkedIn, GitHub), company associations |
|
||||||
| **Trust** | HTTPS, canonical URLs, clear contact info, privacy-respecting analytics |
|
| **Trust** | HTTPS, canonical URLs, clear contact info, privacy-respecting analytics |
|
||||||
|
|
||||||
|
#### 14. Plain Text Version for CLI/TUI Browsers
|
||||||
|
|
||||||
|
**Implementation:** Auto-detect text-based browsers and serve clean plain text.
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Text-based browsers that get plain text automatically
|
||||||
|
var textBrowsers = []string{
|
||||||
|
"curl", "wget", "httpie",
|
||||||
|
"lynx", "w3m", "links", "elinks",
|
||||||
|
"browsh", "carbonyl",
|
||||||
|
}
|
||||||
|
|
||||||
|
func isTextBrowser(r *http.Request) bool {
|
||||||
|
ua := strings.ToLower(r.Header.Get("User-Agent"))
|
||||||
|
for _, browser := range textBrowsers {
|
||||||
|
if strings.Contains(ua, browser) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Also check Accept: text/plain header
|
||||||
|
return strings.HasPrefix(r.Header.Get("Accept"), "text/plain")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage:**
|
||||||
|
```bash
|
||||||
|
# These automatically get plain text:
|
||||||
|
curl https://example.com/ # Detects curl User-Agent
|
||||||
|
wget -qO- https://example.com/ # Detects wget User-Agent
|
||||||
|
lynx https://example.com/ # Text browser gets text version
|
||||||
|
|
||||||
|
# Explicit plain text endpoint:
|
||||||
|
curl https://example.com/text?lang=en
|
||||||
|
```
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- 80-character line wrapping for terminal readability
|
||||||
|
- Centered section titles with ASCII art separators
|
||||||
|
- Clean, structured output (no HTML/CSS/JS)
|
||||||
|
- Preserves all CV content: experience, skills, projects, etc.
|
||||||
|
|
||||||
|
**Benefits:**
|
||||||
|
- ✅ **CLI-friendly:** `curl example.com` just works
|
||||||
|
- ✅ **AI-accessible:** Easy parsing for LLMs and crawlers
|
||||||
|
- ✅ **Accessibility:** Works in any terminal environment
|
||||||
|
- ✅ **No dependencies:** Pure text, no rendering required
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### SEO Files Overview
|
### SEO Files Overview
|
||||||
|
|
||||||
| File | Purpose |
|
| File | Purpose |
|
||||||
@@ -3541,6 +3590,7 @@ Allow: /
|
|||||||
| `static/llms.txt` | AI crawler information file |
|
| `static/llms.txt` | AI crawler information file |
|
||||||
| `static/sitemap.xml` | XML sitemap |
|
| `static/sitemap.xml` | XML sitemap |
|
||||||
| `data/cv-{lang}.json` | SEO fields per language |
|
| `data/cv-{lang}.json` | SEO fields per language |
|
||||||
|
| `/text` endpoint | Plain text CV for CLI/TUI browsers |
|
||||||
|
|
||||||
### Validation
|
### Validation
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ func (h *CVHandler) Home(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Detect text-based browsers and serve plain text version
|
||||||
|
if isTextBrowser(r) {
|
||||||
|
h.PlainText(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Get language from query parameter, default to English
|
// Get language from query parameter, default to English
|
||||||
lang := r.URL.Query().Get("lang")
|
lang := r.URL.Query().Get("lang")
|
||||||
if lang == "" {
|
if lang == "" {
|
||||||
|
|||||||
@@ -10,6 +10,42 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Text-based browsers and CLI tools that should get plain text
|
||||||
|
var textBrowsers = []string{
|
||||||
|
"curl",
|
||||||
|
"wget",
|
||||||
|
"httpie",
|
||||||
|
"lynx",
|
||||||
|
"w3m",
|
||||||
|
"links",
|
||||||
|
"elinks",
|
||||||
|
"browsh",
|
||||||
|
"carbonyl",
|
||||||
|
"netrik",
|
||||||
|
"retawq",
|
||||||
|
"surfraw",
|
||||||
|
}
|
||||||
|
|
||||||
|
// isTextBrowser detects if the request comes from a text-based browser or CLI tool
|
||||||
|
func isTextBrowser(r *http.Request) bool {
|
||||||
|
ua := strings.ToLower(r.Header.Get("User-Agent"))
|
||||||
|
|
||||||
|
// Check for known text browsers
|
||||||
|
for _, browser := range textBrowsers {
|
||||||
|
if strings.Contains(ua, browser) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check Accept header - if client prefers text/plain
|
||||||
|
accept := r.Header.Get("Accept")
|
||||||
|
if strings.HasPrefix(accept, "text/plain") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// ==============================================================================
|
// ==============================================================================
|
||||||
// PLAIN TEXT HANDLER
|
// PLAIN TEXT HANDLER
|
||||||
// Renders CV as clean plain text for terminal/AI consumption
|
// Renders CV as clean plain text for terminal/AI consumption
|
||||||
|
|||||||
Reference in New Issue
Block a user