Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

setu

setu — static URL shortener

Setu (Bengali সেতু, "bridge") is a tiny static URL shortener. You keep a links.json map of slugs to destinations; this package builds a folder of HTML redirect pages that you can host on GitHub Pages (or any static host).

There is no server, database, or serverless function. Each short link is a directory with an index.html that sends the browser onward with a meta refresh and a JavaScript fallback.

This repository is the npm package @to_milon/setu. Generated sites can be hosted on GitHub Pages or any static host.

Why this exists

GitHub Pages only serves static files. It cannot issue HTTP 301 / 302 redirects for arbitrary paths. Generating one HTML page per slug is a reliable workaround:

  1. <meta http-equiv="refresh" content="0; url=…">
  2. window.location.replace(…) if the meta tag is ignored

That is good enough for sharing personal and project links. It is not a replacement for true HTTP redirects when SEO or crawler-critical canonical moves matter.

Requirements

  • Node.js 18.18 or later
  • A GitHub repository if you want to deploy with the included Actions workflow

Install

Use it in its own repo (recommended):

mkdir my-links && cd my-links
npm init -y
npm install --save-dev @to_milon/setu
npx setu init

init writes:

  • links.json — your slug → URL map
  • setu.config.json — site title, base path, optional custom domain
  • .github/workflows/deploy.yml — GitHub Pages deploy on push to main
  • .gitignore — ignores dist/ and node_modules/
  • build / dev scripts on package.json when that file already exists

You can also run the CLI without a local install:

npx @to_milon/setu --help

Quick start

links.json:

{
  "github": "https://github.com/your-username",
  "site": "https://example.com"
}

Build:

npx setu

Preview locally (serves dist/ at http://localhost:3000):

npm run dev

Output:

dist/
  index.html          # listing of every slug
  404.html            # unknown paths
  .nojekyll           # skip Jekyll on GitHub Pages
  CNAME               # only if you set a custom domain
  links.json          # copy of your map
  github/index.html   # redirect page
  site/index.html

After deploy, /github opens the GitHub URL.

Configuration

Values are merged in this order (later wins):

  1. Defaults
  2. setu.config.json
  3. A "setu" object in package.json
  4. CLI flags

Example setu.config.json:

{
  "title": "go.example.com",
  "description": "Short links for Example.",
  "base": "/",
  "cname": "go.example.com",
  "favicon": "https://example.com/favicon.svg",
  "footer": {
    "text": "example.com",
    "href": "https://example.com"
  }
}
Field Type Default Meaning
links string links.json Path to the slug map
out string dist Build directory
base string / URL prefix of the site. Use / on a custom domain or a username.github.io user site. Use /repo-name on a project site at https://username.github.io/repo-name/.
cname string (empty) Custom domain. When set, a CNAME file is written into dist/. Leave empty for *.github.io URLs.
title string Short links Index and 404 heading
description string Static short links. Index subtitle
favicon string (empty) Absolute or site-relative favicon URL
footer object or string (none) { "text", "href" } or raw HTML string
copyLinks boolean true Copy links.json into dist/

CLI

setu [build] [options]
setu init [options]

--links <file>
--out <dir>
--base <path>
--cname <domain>
--title <text>
--description <text>
--favicon <url>
--config <file>
--no-copy-links
--force          # init only: overwrite existing files

Programmatic API

import { build } from '@to_milon/setu';

await build({
  cwd: process.cwd(),
  base: '/my-links',
  title: 'My links',
});

Slug rules

  • Keys in links.json become URL path segments.
  • Allowed: letters, numbers, hyphen, underscore; must start with a letter or number (github, talk-2024, cv ).
  • Destinations must be absolute URLs (https://… or http://…).
  • Slugs are case-preserving on disk; keep them lowercase so links are easy to type.

How GitHub Pages fits

Two different site types matter, because they change the public URL and whether you need a CNAME.

Kind of repo Public URL base cname
Project site (any repo name except username.github.io) https://username.github.io/repo-name/ /repo-name omit
User or org site (repo named username.github.io or org.github.io) https://username.github.io/ / omit
Either kind plus a custom domain https://go.example.com/ / go.example.com

In all cases:

  1. Push the project to GitHub.
  2. Repo Settings → Pages → Build and deployment → Source = GitHub Actions.
  3. Keep the workflow that uploads the dist/ artifact (created by init, or see below).
  4. After the first green run, the Pages URL appears in the Actions log and under Settings → Pages.

The workflow publishes dist/ on every push to main. It also writes .nojekyll so GitHub does not run Jekyll (which would ignore folders that start with _).

name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm install
      - run: npm run build
      - uses: actions/upload-pages-artifact@v3
        with:
          path: dist

  deploy:
    needs: build
    runs-on: ubuntu-latest
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - id: deployment
        uses: actions/deploy-pages@v4

Scenario A — default GitHub Pages domain

Use this when you are fine with https://username.github.io/… and do not own (or do not want to configure) a custom hostname.

A1. Project site: https://username.github.io/repo-name/

This is the usual case: the repo is named something like links or go, not username.github.io.

Example

  • GitHub user: ada
  • Repository: links
  • Short link you want: https://ada.github.io/links/github

Config

{
  "title": "ada’s links",
  "description": "Short links.",
  "base": "/links"
}

Do not set cname. A CNAME file would tell GitHub Pages to serve a custom domain instead of ada.github.io.

Why base must match the repo name

On a project site, GitHub Pages mounts the site at /{repo}/, not at /. Index links, the 404 “view all links” button, and any root-absolute hrefs have to include that prefix. If you leave base as /, a listing link would point at https://ada.github.io/github (missing /links) and 404.

The repo name in the URL is case-sensitive. If the repo is My-Links, use "base": "/My-Links".

Checklist

  1. Create the GitHub repo links (public, unless you have GitHub Pro/Team/Enterprise Pages for private repos).
  2. Commit links.json, setu.config.json with "base": "/links", package.json, and the workflow.
  3. Settings → Pages → Source = GitHub Actions.
  4. Push to main and wait for the Deploy workflow.
  5. Open https://ada.github.io/links/ for the listing and https://ada.github.io/links/github for a slug.
  6. If you see a 404 on /links/ but /links/index.html works, wait a minute and hard-refresh; the first Pages deploy can lag. Confirm the workflow uploaded dist/, not the repo root.

Local preview note

npx serve dist always serves at /. For a faithful local check of a project site, either:

  • build with --base / while iterating locally, or
  • put dist/ behind a folder named like the repo (mkdir -p preview/links && cp -R dist/* preview/links/ && npx serve preview) so /links/github resolves.

A2. User site: https://username.github.io/

If the repository is named exactly username.github.io (for example ada.github.io), GitHub Pages serves it at the domain root.

Config

{
  "title": "ada.github.io",
  "base": "/"
}

Still no cname. Short links look like https://ada.github.io/github.

This repo name is special: it can also be deployed from the main (or docs) branch without Actions, but using the Actions workflow here is simpler and matches the project-site setup.


Scenario B — custom domain

Use this when you want https://go.example.com/github instead of a github.io URL. link.milon.im is an example of that setup.

Custom domains work with either a project repo or a username.github.io repo. Once the domain is attached, GitHub serves the site at the domain root, so base should be / even if the repo is named links.

Example

  • Domain: go.example.com
  • Desired link: https://go.example.com/github

Config

{
  "title": "go.example.com",
  "description": "Short links.",
  "base": "/",
  "cname": "go.example.com"
}

The build writes dist/CNAME containing go.example.com. GitHub Pages reads that file and associates the domain with the site.

DNS

Pick one of these. Do not mix conflicting records.

Subdomain (go.example.com, recommended)

At your DNS host, add a CNAME record:

Type Name / host Value
CNAME go username.github.io

The value is always username.github.io (or org.github.io), not username.github.io/repo. GitHub maps the hostname to the right repository using the CNAME file and Pages settings.

Apex domain (example.com)

GitHub documents A (and optionally AAAA) records to their Pages IPs. Apex CNAMEs are often unsupported by DNS providers. If you use the apex, also add www as a CNAME to username.github.io and choose a primary domain in Pages settings.

TTL: start with a low TTL (300 seconds) until the domain works, then raise it.

GitHub settings

  1. Deploy once so CNAME exists on the Pages site (or type the domain under Settings → Pages → Custom domain and save; GitHub will commit a CNAME if you use branch deploys — with Actions, prefer generating it from setu.config.json so it does not get wiped on the next build).
  2. Settings → Pages → Custom domain = go.example.com.
  3. Wait for DNS check to pass. Failures are usually a wrong CNAME target, a leftover A record, or DNS not propagated.
  4. Enable Enforce HTTPS. Certificate provisioning can take several minutes after DNS is correct. Do not toggle the domain on and off while it is issuing.
  5. If the site was previously available at https://username.github.io/repo/, GitHub will redirect that URL to the custom domain.

Checklist

  1. "base": "/" and "cname": "go.example.com" in config.
  2. DNS CNAME gousername.github.io.
  3. Pages source = GitHub Actions; workflow green.
  4. Pages custom domain shows a checkmark; HTTPS is enforced.
  5. https://go.example.com/ lists slugs; https://go.example.com/github redirects.

Switching from a default github.io URL to a custom domain

  1. Change base from /repo-name to /.
  2. Set cname.
  3. Redeploy, then add DNS and the Pages custom domain.
  4. Old links under https://username.github.io/repo-name/slug will stop matching unless you keep base as /repo-name — which you should not do on a custom domain, because https://go.example.com/repo-name/slug would be the path. Plan a cutover; update any already-shared URLs.

Switching the other way

Remove cname from config (so no CNAME file is emitted), delete the custom domain in Pages settings, and set base back to /repo-name for a project site. Clear leftover DNS records so they do not keep pointing at GitHub.

Limitations

  • Redirects are client-side, not HTTP 301 / 302.
  • Unknown slugs depend on GitHub Pages serving 404.html for missing paths (this is the default).
  • There are no click analytics unless you add your own (for example a privacy-friendly script on the redirect template).
  • Query strings and hash fragments on the short URL are not forwarded today; the destination is exactly the string in links.json.

License

MIT

About

Setu (সেতু, bridge): static URL shortener for GitHub Pages

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages