x-article-reader
Val Town is a collaborative website to build and scale JavaScript apps.
Deploy APIs, crons, & store data – all from the browser, and deployed in milliseconds.
Viewing readonly version of main branch: v127View latest version
A Val Town application that formats X.com (formerly Twitter) article-style posts in a clean, aesthetically pleasing reading experience. The application fetches content from the FXTwitter API and displays it with minimal black and white design, optimized for readability.
- Pattern:
x.com/user/status/id→subdomain.val.run/user/status/id - Support: Accept both
x.comandtwitter.comdomains - Normalization: Internally normalize all URLs to x.com format before processing
- Primary Interface: Interactive web page with URL input form
- Homepage: Landing page with description, input form, and instructional examples showing the URL pattern
- Example Content: Balance between showing example threads and clear instructions on how to use the service
- Target Content: Article threads only (long-form thread articles)
- Article Format: API provides article content in a single structure (not individual threaded tweets)
- Display: Single continuous article view
- Style: Minimal black and white design
- Purpose: Distraction-free, focus entirely on readability and content
- Color Palette: Pure black text on white background, no dynamic theming from API color data
- Font Family: Modern sans-serif (system fonts for fast loading)
- Font Stack:
system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif - Content Width: Maximum ~650px for optimal readability (65-75 characters per line)
- Responsive: Centered content with padding on larger screens
- Hero Image: Cover media displayed as large hero image at top
- Article Title: Large H1 heading below hero image
- Author Information: Display name + avatar image
- Metadata: Publication date (relative format: "2 hours ago", "yesterday" for recent; full date for older)
- Article Content: Full formatted content with proper spacing
- Footer: Link to view original on X.com
- Rendering Style: Full formatting support
- Supported Elements:
- Headers (H2/H3 for header-two, etc.)
- Unordered and ordered lists (
<ul>,<ol>) - Bold and italic text via inline style ranges
- Code blocks with syntax highlighting
- Links (converted from entity ranges)
- Paragraphs with proper spacing
- Support Level: Full entity support
- Entity Types:
- URLs → clickable links with appropriate attributes
- Code blocks → properly formatted with syntax highlighting
- Mentions → formatted appropriately
- All entity ranges from entityMap processed correctly
- Images: Full media display inline within article
- Cover Image: Displayed as prominent hero image
- Embedded Media: All images from content shown at full width within content container
- Loading: Load all media immediately (no lazy loading)
- Library: Highlight.js
- Approach: Load core library + only needed languages for minimal bundle size
- Theme: Minimal, monochrome theme matching the black/white aesthetic
- Features: Basic syntax highlighting without line numbers or extra UI
- Type: HTTP Val (
fileType: "http") - Runtime: Deno serverless environment
- Framework: Hono for routing and API endpoints
GET / → Homepage with input form
GET /:username/status/:id → Formatted article view
- Parse URL to extract username and status ID
- Check SQLite cache for existing article data
- If not cached or expired (>24 hours), fetch from FXTwitter API
- Store response in SQLite cache with timestamp
- Parse API response and render HTML
- Return formatted article page
- Backend: Hono for routing
- Frontend: Highlight.js for syntax highlighting (minimal build)
- API: FXTwitter API (
https://api.fxtwitter.com/:username/status/:id) - Styling: Pure CSS, no frameworks
- JavaScript: None on article pages (pure static HTML)
CREATE TABLE IF NOT EXISTS articles (
tweet_id TEXT PRIMARY KEY,
fetched_at INTEGER NOT NULL,
data TEXT NOT NULL
);
- Key: Tweet status ID (unique identifier)
- TTL: 24 hours
- Expiration Logic: Refetch articles older than 24 hours
- Storage Format: JSON string of complete FXTwitter API response
- Invalidation: Time-based only (no manual invalidation)
// Check cache
const cached = await sqlite.execute({
sql: "SELECT data, fetched_at FROM articles WHERE tweet_id = ?",
args: [tweetId]
});
// Is cache valid?
const cacheAge = Date.now() - cached.rows[0].fetched_at;
const isExpired = cacheAge > 24 * 60 * 60 * 1000; // 24 hours
// Store in cache
await sqlite.execute({
sql: "INSERT OR REPLACE INTO articles (tweet_id, fetched_at, data) VALUES (?, ?, ?)",
args: [tweetId, Date.now(), JSON.stringify(apiResponse)]
});
- Endpoint:
https://api.fxtwitter.com/:username/status/:id - Response Structure: See example at
https://api.fxtwitter.com/francedot/status/2015178880215298557
interface APIResponse {
code: number;
message: string;
tweet: {
url: string;
id: string;
author: {
name: string;
screen_name: string;
avatar_url: string;
};
article: {
title: string;
created_at: string;
cover_media: {
url: string;
width: number;
height: number;
};
content: {
blocks: Array<{
type: string; // "unstyled", "header-two", "unordered-list-item", etc.
text: string;
inlineStyleRanges: Array<{
offset: number;
length: number;
style: string; // "BOLD", "ITALIC", etc.
}>;
entityRanges: Array<{
offset: number;
length: number;
key: number;
}>;
}>;
entityMap: {
[key: string]: {
type: string; // "LINK", "CODE", etc.
data: any;
};
};
};
};
};
}
- Invalid URL: Not a valid X.com/Twitter.com article URL
- Deleted Tweet: FXTwitter returns 404
- Private Account: FXTwitter returns access error
- API Failure: FXTwitter API is down or unresponsive
- Not an Article: Tweet exists but is not an article-style post
- UI: Clean, minimal error page
- Content:
- Clear explanation of what went wrong
- Link to go back to original article on x.com
- Consistent styling with main design
- HTTP Status: Appropriate status codes (404, 500, etc.)
- No Retry Logic: Simple failure, no automatic retries
- Stale Cache: Do not serve expired cached content on API failure
<div class="error-container"> <h1>Unable to load article</h1> <p>[Contextual error message]</p> <a href="[original x.com URL]">View on X.com</a> </div>
- Service Title/Logo: Clear branding for the service
- Description: Brief explanation of what the service does
- URL Pattern Example: Show the pattern:
x.com/user/status/id→subdomain.val.run/user/status/id - Input Form: URL input field with submit button
- Example Links: 1-2 example article URLs demonstrating the service
- Input: Text field accepting X.com or Twitter.com URLs
- Validation: Check if URL matches expected pattern
- Action: Submit button that redirects to formatted article path
- Transformation: Extract username and status ID, navigate to
/:username/status/:id
User pastes: https://x.com/francedot/status/2015178880215298557
Form extracts: username=francedot, id=2015178880215298557
Redirects to: /francedot/status/2015178880215298557
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>[Article Title] - [Author Name]</title> <!-- OpenGraph tags --> <meta property="og:title" content="[Article Title]"> <meta property="og:description" content="[Preview text or first paragraph]"> <meta property="og:image" content="[Cover media URL]"> <meta property="og:type" content="article"> <!-- Styles --> <link rel="stylesheet" href="[styles.css or inline]"> <!-- Highlight.js for syntax highlighting --> <link rel="stylesheet" href="[highlight.js theme]"> <script src="[highlight.js core + languages]"></script> </head> <body> <article class="article-container"> <img src="[cover_media.url]" alt="Cover image" class="hero-image"> <header class="article-header"> <h1 class="article-title">[Article Title]</h1> <div class="author-info"> <img src="[author.avatar_url]" alt="[author.name]" class="avatar"> <span class="author-name">[author.name]</span> </div> <time class="publish-date">[Relative date]</time> </header> <div class="article-content"> [Rendered blocks with full formatting] </div> <footer class="article-footer"> <a href="[original tweet URL]" target="_blank" rel="noopener"> View original on X.com </a> </footer> </article> </body> </html>
const blockTypeMap = {
'unstyled': 'p',
'header-one': 'h1',
'header-two': 'h2',
'header-three': 'h3',
'unordered-list-item': 'li', // wrap in <ul>
'ordered-list-item': 'li', // wrap in <ol>
'atomic': 'div', // special handling for embedded content
'code-block': 'pre' // wrap in <code> with highlighting
};
function applyInlineStyles(text: string, ranges: InlineStyleRange[]): string {
// Sort ranges by offset
// Apply BOLD → <strong>, ITALIC → <em>, etc.
// Handle overlapping ranges correctly
// Return HTML string with inline formatting
}
function applyEntityRanges(text: string, ranges: EntityRange[], entityMap: EntityMap): string {
// For each entity range:
// - Extract entity from entityMap by key
// - Apply appropriate transformation:
// - LINK → <a href="...">text</a>
// - CODE → <code>text</code>
// - Handle other entity types as needed
// Return HTML string with entities processed
}
- Load Everything: Render complete article at once (no pagination or lazy loading)
- Minimal Dependencies: Only essential libraries loaded
- SQLite Cache: Reduces API calls and improves response time for repeated views
- Static HTML: No client-side JavaScript on article pages (except Highlight.js initialization)
- Content Width: Fixed max-width prevents excessive line length and maintains readability
- Core HTML/CSS: < 10KB
- Highlight.js: < 50KB (core + common languages only)
- Total Initial Load: < 100KB excluding images
- Time to First Byte: < 200ms (cached), < 1s (uncached)
- No Tracking: No analytics or user behavior logging
- No Cookies: No cookies set or required
- No Third-Party Scripts: Only self-hosted or CDN resources (Highlight.js)
- No User Data Storage: Only article content cached, no user identifiers
- Application Logs: Server-side errors logged for debugging
- No Access Logs: Do not log article views or user behavior
- Error Tracking: Only log failed API requests for debugging purposes
<meta property="og:title" content="[Article Title]"> <meta property="og:description" content="[Article preview_text or excerpt]"> <meta property="og:image" content="[cover_media.url]"> <meta property="og:type" content="article"> <meta property="og:url" content="[Current URL]">
<title>[Article Title] by [Author Name]</title>
function formatDate(timestamp: string): string {
const date = new Date(timestamp);
const now = new Date();
const diffInHours = (now - date) / (1000 * 60 * 60);
if (diffInHours < 1) return "Just now";
if (diffInHours < 24) return `${Math.floor(diffInHours)} hours ago`;
if (diffInHours < 48) return "Yesterday";
if (diffInHours < 168) return `${Math.floor(diffInHours / 24)} days ago`;
// For older articles, show full date
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
- Set up Hono app with route handlers
- Implement SQLite schema and caching logic
- Create FXTwitter API client with error handling
- Build URL parser for both x.com and twitter.com
- Implement cache lookup and storage
- Create article renderer (blocks → HTML)
- Build error page template
- Design minimal CSS stylesheet
- Create homepage HTML template
- Build article page HTML template
- Integrate Highlight.js for syntax highlighting
- Implement inline style ranges rendering
- Implement entity ranges rendering
- Add OpenGraph meta tags
- Style error pages
- Test with various article types
- Verify cache expiration logic
- Test error scenarios (404, API down, etc.)
- Validate URL parsing for both domains
- Check rendering of all block types
- Verify syntax highlighting works
- Test social sharing metadata
- Mobile responsiveness testing
- Deploy to Val Town
- Verify endpoint URL
- Test live with real X.com articles
- Monitor for errors
- Document usage
- Dark mode toggle
- Reader preferences (font size, width)
- Article bookmarking/favorites
- RSS feed generation
- Print-optimized styling
- Reading time estimates
- Multi-language support
- Search functionality
- Article collections/categorization
- Functionality: Successfully formats X.com article threads with proper rendering
- Performance: Pages load in < 2 seconds including images
- Readability: Clean, distraction-free reading experience
- Reliability: Graceful error handling for all edge cases
- Simplicity: Minimal dependencies, straightforward codebase
- Privacy: No tracking or unnecessary data collection