A lightweight URL shortening service built with Hono that provides a web interface for creating shortened links and serves HTML pages with Open Graph and Twitter Card meta tags for proper social media link unfurling. The service fetches data from the Intuition protocol's GraphQL API and dynamically generates SEO-optimized pages for atoms and triples with automatic type detection.
- 🚀 Fast & Lightweight: Built with Hono framework (5KB)
- 📝 Web Interface: User-friendly form for creating shortened URLs
- ⚡ Smart URL Compression: Shortest prefix algorithm with base62 encoding produces ultra-short URLs (e.g.,
9LE) - 🔗 Social Media Optimized: Proper Open Graph and Twitter Card meta tags
- 🎨 Dynamic Content: Meta tags populated from GraphQL data
- 🤖 Auto Type Detection: Automatically detects atoms vs triples
- 🔄 Auto-redirect: Seamless redirect to Intuition Portal
- 📦 TypeScript: Full type safety with TypeScript
- ☁️ Deploy Ready: Configured for Render.com deployment
GET /- URL shortener form homepageGET /about- How it works pageGET /health- Health check endpointGET /short- Create shortened URL (accepts URL query parameter)GET /:predicateId/:objectId- List redirect with social meta tagsGET /:id- Unified redirect for atoms and triples with social meta tags
GET /api/short/term/:termId- Returns shortened URL as plain text for a single term (atom or triple)GET /api/short/list/:predicateTermId/:objectTermId- Returns shortened URL as plain text for lists
- Framework: Hono v4.x
- Runtime: Node.js via @hono/node-server
- GraphQL Client: graphql-request
- Templates: Hono JSX
- Language: TypeScript
- Node.js >= 18.0.0
- npm or yarn
- Clone the repository:
git clone <repository-url>
cd intuition-url-shortener- Install dependencies:
npm install- Create environment file (optional):
cp .env.example .envEdit .env if you need to customize:
GRAPHQL_ENDPOINT=https://mainnet.intuition.sh/v1/graphql
PORT=3000
NODE_ENV=development- Start development server:
npm run devThe server will start at http://localhost:3000 with hot reload enabled.
Visit these URLs to test:
- Homepage form:
http://localhost:3000/ - Health check:
http://localhost:3000/health - Direct atom link:
http://localhost:3000/0x8c486fd3377cef67861f7137bcc89b188c7f1781314e393e22c1fa6fa24e520e - Direct triple link:
http://localhost:3000/{triple-id} - Direct list link:
http://localhost:3000/8RP/9Vk
To test the URL shortener form:
- Visit
http://localhost:3000/ - Paste a portal URL:
- Atom:
https://portal.intuition.systems/explore/atom/0x8c486fd3377cef67861f7137bcc89b188c7f1781314e393e22c1fa6fa24e520e - Triple:
https://portal.intuition.systems/explore/triple/{triple-id} - List:
https://portal.intuition.systems/explore/list/0x7ec36d201c842dc787b45cb5bb753bea4cf849be3908fb1b0a7d067c3c3cc1f5-0x8ed4f8de1491e074fa188b5c679ee45c657e0802c186e3bb45a4d3f3faa6d426
- Atom:
- Click "Shorten URL"
- View the preview and copy the shortened link
npm run buildThis compiles TypeScript to JavaScript in the dist/ directory.
To run the production build:
npm startThis project includes a render.yaml configuration file for easy deployment to Render.com.
Option 1: Deploy via Dashboard
- Create a new Web Service on Render.com
- Connect your GitHub repository
- Render will automatically detect the
render.yamlfile - Click "Apply" to deploy
Option 2: Deploy via Blueprint
- Click the "Deploy to Render" button (if available)
- Connect your GitHub account
- Render will automatically configure everything from
render.yaml
Environment Variables
The following environment variables are pre-configured in render.yaml:
NODE_ENV=productionGRAPHQL_ENDPOINT=https://mainnet.intuition.sh/v1/graphqlPORT=10000
You can override these in the Render dashboard if needed.
intuition-url-shortener/
├── src/
│ ├── index.ts # Main Hono application
│ ├── server.ts # Node.js server entry point
│ ├── routes/
│ │ ├── home.tsx # GET / - Homepage form
│ │ ├── shortener.tsx # GET /short - Form handler
│ │ ├── api.tsx # API endpoints (plain text responses)
│ │ ├── about.tsx # GET /about - How it works page
│ │ ├── list.tsx # GET /:predicateId/:objectId - List redirect
│ │ ├── term.tsx # GET /:id - Unified redirect
│ │ └── error.tsx # 404 handler
│ ├── services/
│ │ └── graphql.ts # GraphQL client & queries
│ ├── components/
│ │ ├── MetaTags.tsx # Meta tag component
│ │ ├── HomePage.tsx # URL shortener form
│ │ ├── PreviewPage.tsx # Preview with copy button
│ │ ├── RedirectPage.tsx # Unified redirect template
│ │ ├── AboutPage.tsx # How it works page
│ │ └── ErrorPage.tsx # Error page template
│ ├── types/
│ │ └── graphql.ts # GraphQL response types
│ └── utils/
│ ├── env.ts # Environment config
│ ├── metadata.ts # Type detection & extraction
│ ├── urlParser.ts # URL parsing utility
│ ├── prefixFinder.ts # Shortest prefix algorithm
│ ├── base62.ts # Base62 encoding/decoding
│ ├── idDetector.ts # ID format detection
│ └── shortener.ts # Shared shortening logic for API/web
├── plans/ # Reference files
│ ├── example.html
│ └── query.graphql
├── package.json
├── tsconfig.json
├── render.yaml # Render.com config
├── .env.example
├── .gitignore
└── README.md
For Atoms and Triples:
- User visits
/and sees the URL shortener form - User pastes an Intuition Portal URL
- Form submits to
/shortwhich extracts the ID using regex patterns - Server queries Intuition's GraphQL API for the term data
- Shortest Prefix Algorithm: Server finds the shortest hex prefix that uniquely identifies the term (starts with 2 chars, uses smart character comparison to minimize API calls)
- Server encodes the shortest prefix to base62 for maximum URL compression
- Server displays a preview page showing:
- Share card preview (title, description, image)
- Shortened URL (e.g.,
http://localhost:3000/9LE) - Copy button for easy sharing
For Lists:
- User visits
/and sees the URL shortener form - User pastes a list URL:
portal.intuition.systems/explore/list/{predicateId}-{objectId} - Form submits to
/shortwhich extracts both predicate and object IDs - Server queries GraphQL for both terms in parallel
- Server finds shortest prefix for EACH ID separately
- Server encodes both prefixes to base62
- Server displays preview using object term's metadata
- Shortened URL format:
/{base62PredicateId}/{base62ObjectId}(e.g.,http://localhost:3000/8RP/9Vk) - List image URL uses full hex IDs:
portal.intuition.systems/resources/list-image?id={fullPredicateId}-{fullObjectId}
For Atoms and Triples (/:id):
- User or social media bot visits shortened URL (e.g.,
/:id) - Server detects ID format (hex with
0xprefix or base62 alphanumeric) - If base62, decodes to hex prefix (preserving short prefixes)
- Server queries Intuition's GraphQL API using prefix matching (takes first result ordered by creation date)
- Server automatically detects whether it's an atom or triple
- Server extracts appropriate metadata (title, description)
- Server renders HTML with Open Graph and Twitter Card meta tags
- Social media crawlers see the meta tags for link previews
- User is automatically redirected to the full portal URL
For Lists (/:predicateId/:objectId):
- User or social media bot visits list URL (e.g.,
/8RP/9Vk) - Server detects format of both IDs and decodes if base62
- Server queries GraphQL for both terms in parallel
- Server extracts metadata from object term (title, description)
- Server generates list image URL with full hex IDs
- Server renders HTML with meta tags pointing to list image
- User is redirected to:
portal.intuition.systems/explore/list/{fullPredicateId}-{fullObjectId}
The URL shortener uses an intelligent algorithm to minimize URL length:
- Start Small: Begins with a 2-character hex prefix (e.g.,
0x8c) - Smart Comparison: When collisions occur, compares IDs character-by-character to calculate exactly how many characters are needed
- Jump to Target: Instead of blindly incrementing, jumps directly to the required length
- Minimize API Calls: Typically requires only 1-3 GraphQL queries to find the optimal prefix
- Base62 Encoding: Encodes the shortest hex prefix to base62 for maximum compression
Example:
- Full ID:
0x8c486fd3377cef67861f7137bcc89b188c7f1781314e393e22c1fa6fa24e520e - Shortest prefix:
0x8c48(4 hex chars) - Base62 encoded:
9LE(3 chars) - Result: 97% shorter URL!
The algorithm leverages the fact that GraphQL results are ordered by creation date (oldest first), ensuring that partial IDs always resolve deterministically to the same term.
The service uses three redirect methods to ensure compatibility:
- Meta refresh:
<meta http-equiv="refresh">(browser fallback) - JavaScript redirect:
window.location.href(immediate) - Visible link: Clickable link as last resort
This ensures social media crawlers can read meta tags before the redirect executes.
The URL shortener provides REST API endpoints that return plain text shortened URLs for programmatic access.
# Using full hex ID
curl http://localhost:3000/api/short/term/0x8c486fd3377cef67861f7137bcc89b188c7f1781314e393e22c1fa6fa24e520e
# Returns: http://localhost:3000/9LE
# Using partial hex ID
curl http://localhost:3000/api/short/term/0x8c486fd3377
# Returns: http://localhost:3000/9LE
# Using base62 ID
curl http://localhost:3000/api/short/term/9LE
# Returns: http://localhost:3000/9LE# Using full hex IDs
curl "http://localhost:3000/api/short/list/0x7ec36d201c842dc787b45cb5bb753bea4cf849be3908fb1b0a7d067c3c3cc1f5/0x8ed4f8de1491e074fa188b5c679ee45c657e0802c186e3bb45a4d3f3faa6d426"
# Returns: http://localhost:3000/8RP/9Vk
# Using base62 IDs
curl http://localhost:3000/api/short/list/8RP/9Vk
# Returns: http://localhost:3000/8RP/9Vk- Format Detection: Accepts both hex (full or partial) and base62 IDs
- Plain Text Response: Returns the shortened URL as plain text for easy parsing
- Error Handling: Returns 404 with descriptive error messages for invalid or missing terms
- Parallel Processing: List endpoint fetches both terms simultaneously for optimal performance
# Invalid term ID
curl http://localhost:3000/api/short/term/invalid-id
# Returns: Error: Term not found (404)
# Missing list term
curl http://localhost:3000/api/short/list/invalid1/invalid2
# Returns: Error: One or both terms not found (404)The service uses the following GraphQL query:
query GetTerm($id: String!) {
terms(where: { id: { _like: $id } }) {
id
atom {
label
value {
json_object {
description: data(path:"description")
}
}
}
triple {
subject { label, value { ... } }
predicate { label, value { ... } }
object { label, value { ... } }
}
}
}Use these tools to validate link unfurling:
- Twitter: Twitter Card Validator
- Facebook: Sharing Debugger
- LinkedIn: Post Inspector
Render.com's free tier apps sleep after 15 minutes of inactivity. First request may take ~30 seconds. Consider upgrading to a paid tier for always-on service.
Social media platforms cache meta tags. Use the validation tools above to refresh the cache during testing.
Ensure all .tsx files are in the src/ directory and jsxImportSource is set to hono/jsx in tsconfig.json.
npm run dev- Start development server with hot reloadnpm run build- Compile TypeScript to JavaScriptnpm start- Run production servernpm run type-check- Run TypeScript type checking without emitting files
MIT
Contributions are welcome! Please follow these steps:
- Fork the repository
- Create a feature branch
- Make your changes
- Test thoroughly
- Submit a pull request
For issues and questions:
- GitHub Issues: Create an issue
- Intuition Portal: Visit portal.intuition.systems