Build absolute and canonical URLs from a base — for SSR/SSG canonical links, OG image URLs, JSON-LD and sitemaps. Zero dependencies.
Works in Node.js, Cloudflare Workers, Deno, Bun, and browsers.
npm install @arraypress/site-urlIn a static/SSR site you constantly need absolute URLs (canonical <link>, og:image, JSON-LD url/@id, sitemap entries). It's tempting to hand-roll them:
const origin = site?.toString().replace(/\/$/, '') ?? '';
const url = `${origin}${path}`; // brittle: no fallback, double-slash bugsThis package does it correctly with the WHATWG URL constructor, takes the base as an argument (so it's framework-agnostic), and falls back gracefully when the base is unset.
Resolve path to an absolute URL against base (falling back to fallback). If path is already absolute the base is ignored; if no base is resolvable, path is returned unchanged.
import { absUrl } from '@arraypress/site-url';
absUrl('/og.png', 'https://example.com') // 'https://example.com/og.png'
absUrl('/x', 'https://example.com/') // 'https://example.com/x' (no double slash)
absUrl('https://cdn.test/i.png', 'https://x.com')// 'https://cdn.test/i.png' (already absolute)
absUrl('/x') // '/x' (no base → unchanged)The origin of base (scheme + host, no trailing slash), or '' if unresolvable.
import { originOf } from '@arraypress/site-url';
originOf('https://example.com/some/path') // 'https://example.com'Join a base and path segments with single slashes, skipping empty/nullish segments. No trailing slash is added.
import { joinUrl } from '@arraypress/site-url';
joinUrl('https://x.com/a/', '/b/', 'c') // 'https://x.com/a/b/c'
joinUrl('/types', 'zodiac', 'leo') // '/types/zodiac/leo'
joinUrl('/types/enneagram', 4) // '/types/enneagram/4'Normalise trailing slashes for canonical URLs. ensureTrailingSlash leaves URLs with a query/hash untouched.
import { stripTrailingSlash, ensureTrailingSlash } from '@arraypress/site-url';
stripTrailingSlash('https://x.com/a/') // 'https://x.com/a'
stripTrailingSlash('/') // '/'
ensureTrailingSlash('https://x.com/a') // 'https://x.com/a/'Astro.site and Astro.url are only available inside .astro frontmatter, so pass them in. Use Astro.site as the base and Astro.url as the fallback, so URLs still resolve in dev/preview where site may be unset.
---
import { absUrl, originOf } from '@arraypress/site-url';
const path = Astro.url.pathname;
const canonical = absUrl(path, Astro.site, Astro.url);
const ogImage = absUrl('/og/home.png', Astro.site, Astro.url);
const origin = originOf(Astro.site, Astro.url);
---
<link rel="canonical" href={canonical} />
<meta property="og:image" content={ogImage} />MIT © ArrayPress Limited