1
mirror of https://github.com/jakejarvis/get-canonical-url.git synced 2025-04-25 17:45:25 -04:00

initial commit 🎉

This commit is contained in:
Jake Jarvis 2021-09-29 20:49:54 -04:00
commit 0a7a95b30c
Signed by: jake
GPG Key ID: 2B0C9CF251E69A39
17 changed files with 5124 additions and 0 deletions

17
.editorconfig Normal file
View File

@ -0,0 +1,17 @@
# http://editorconfig.org
# this file is the top-most editorconfig file
root = true
# all files
[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
# site content
[*.md]
trim_trailing_whitespace = false

19
.eslintrc.json Normal file
View File

@ -0,0 +1,19 @@
{
"extends": [
"@jakejarvis/eslint-config"
],
"parser": "@babel/eslint-parser",
"parserOptions": {
"sourceType": "module",
"requireConfigFile": false
},
"env": {
"es6": true,
"browser": true,
"node": true
},
"ignorePatterns": [
"*.d.ts",
"dist/**"
]
}

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Set default behavior to automatically normalize line endings.
* text=auto eol=lf

10
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,10 @@
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
versioning-strategy: increase
schedule:
interval: "daily"
commit-message:
prefix: "📦 npm:"

20
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,20 @@
name: CI
on:
push:
branches:
- main
pull_request:
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14.x'
- run: yarn install --frozen-lockfile
- run: yarn build
- run: yarn test
- run: yarn lint

22
.github/workflows/release.yml vendored Normal file
View File

@ -0,0 +1,22 @@
name: Release
on:
push:
tags:
- 'v*'
jobs:
npm:
name: Publish to NPM
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: 14
registry-url: https://registry.npmjs.org/
- env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
yarn install --frozen-lockfile
yarn publish

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.DS_Store
node_modules/
dist/
.npmrc
.vscode/

19
LICENSE Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2021 Jake Jarvis
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

101
README.md Normal file
View File

@ -0,0 +1,101 @@
# 🔗 get-canonical-url
[![CI](https://github.com/jakejarvis/get-canonical-url/actions/workflows/ci.yml/badge.svg)](https://github.com/jakejarvis/get-canonical-url/actions/workflows/ci.yml)
[![npm](https://img.shields.io/npm/v/get-canonical-url?logo=npm)](https://www.npmjs.com/package/get-canonical-url)
[![MIT License](https://img.shields.io/github/license/jakejarvis/get-canonical-url?color=violet)](LICENSE)
Determines the current page's canonical URL and optionally normalizes it via [normalize-url](https://github.com/sindresorhus/normalize-url) for consistency.
## Install
```sh
npm install get-canonical-url
# or...
yarn add get-canonical-url
```
## Usage
### With `<link rel="canonical">`
```html
<!doctype html>
<html>
<head>
<link rel="canonical" href="https://www.example.com/this/doesnt/exist.aspx?no=really&it=doesnt#gocheck">
</head>
</html>
```
```js
import canonicalUrl from "get-canonical-url";
canonicalUrl();
//=> 'https://www.example.com/this/doesnt/exist.aspx?no=really&it=doesnt#gocheck'
canonicalUrl({
normalize: true,
normalizeOptions: {
stripProtocol: true,
stripWWW: true,
stripHash: true
}
})
//=> 'example.com/this/doesnt/exist.aspx?it=doesnt&no=really'
```
### Without `<link rel="canonical">`
```js
import canonicalUrl from "get-canonical-url";
canonicalUrl({
guess: true,
normalize: true
});
//=> Determined via the individual user's window.location.href or other similar browser hints. Normalizing is recommended.
```
## API
### canonicalUrl(options?)
#### options
Type: `object`
##### normalize
Type: `boolean`\
Default: `false`
Clean-up and normalize the determined canonical URL.
##### normalizeOptions
Type: [`NormalizeOptions`](https://github.com/sindresorhus/normalize-url/blob/main/index.d.ts)\
Default:
```js
{
stripWWW: false,
stripHash: true,
removeQueryParameters: true,
removeTrailingSlash: false
}
```
Options passed directly to [`normalize-url`](https://github.com/sindresorhus/normalize-url#options).
Requires `options.normalize = true`.
##### guess
Type: `boolean`\
Default: `false`
Make an educated guess using other clues if canonical isn't explicitly set in the page's `<head>`.
## License
MIT

73
package.json Normal file
View File

@ -0,0 +1,73 @@
{
"name": "get-canonical-url",
"version": "0.0.0",
"description": "🔗 Determines the current page's canonical URL and optionally normalizes it for consistency.",
"license": "MIT",
"homepage": "https://github.com/jakejarvis/get-canonical-url",
"author": {
"name": "Jake Jarvis",
"email": "jake@jarv.is",
"url": "https://jarv.is/"
},
"repository": {
"type": "git",
"url": "https://github.com/jakejarvis/get-canonical-url.git"
},
"type": "module",
"files": [
"dist"
],
"main": "./dist/get-canonical-url.cjs.js",
"module": "./dist/get-canonical-url.esm.js",
"unpkg": "./dist/get-canonical-url.min.js",
"types": "./dist/get-canonical-url.d.ts",
"exports": {
"require": "./dist/get-canonical-url.cjs.js",
"import": "./dist/get-canonical-url.esm.js",
"browser": "./dist/get-canonical-url.min.js"
},
"scripts": {
"build": "rollup -c",
"watch": "rollup -c -w",
"test": "mocha",
"lint": "eslint .",
"prepublishOnly": "yarn build"
},
"dependencies": {
"normalize-url": "^7.0.2"
},
"devDependencies": {
"@babel/core": "^7.15.5",
"@babel/eslint-parser": "^7.15.7",
"@babel/preset-env": "^7.15.6",
"@jakejarvis/eslint-config": "*",
"@rollup/plugin-babel": "^5.3.0",
"@rollup/plugin-eslint": "^8.0.1",
"@rollup/plugin-node-resolve": "^13.0.5",
"@types/chai": "^4.2.22",
"@types/jsdom": "^16.2.13",
"@types/mocha": "^9.0.0",
"chai": "^4.3.4",
"eslint": "^7.32.0",
"eslint-plugin-compat": "~3.13.0",
"eslint-plugin-import": "~2.24.2",
"jsdom": "^17.0.0",
"mocha": "^9.1.2",
"rollup": "^2.57.0",
"rollup-plugin-copy": "^3.4.0",
"rollup-plugin-delete": "^2.0.0",
"rollup-plugin-filesize": "^9.1.1",
"rollup-plugin-terser": "^7.0.2"
},
"keywords": [
"url",
"uri",
"canonical",
"link",
"address",
"dom",
"browser",
"normalize",
"front-end"
]
}

92
rollup.config.js Normal file
View File

@ -0,0 +1,92 @@
import pkg from "./package.json";
import resolve from "@rollup/plugin-node-resolve";
import { babel } from "@rollup/plugin-babel";
import { terser } from "rollup-plugin-terser";
import eslint from "@rollup/plugin-eslint";
import filesize from "rollup-plugin-filesize";
import copy from "rollup-plugin-copy";
import del from "rollup-plugin-delete";
const exportName = "canonicalUrl";
const input = "src/index.js";
const banner = `/*! ${pkg.name} v${pkg.version} | ${pkg.license} | ${pkg.homepage} */`;
export default [
{
// universal (browser and node)
input,
output: [
{
name: exportName,
file: pkg.exports.browser.replace(".min.js", ".js"), // unminified (.js)
format: "umd",
exports: "default",
esModule: false,
banner,
},
{
name: exportName,
file: pkg.exports.browser, // minified (.min.js)
format: "umd",
exports: "default",
esModule: false,
plugins: [
terser({
format: {
preamble: banner,
},
}),
],
},
],
plugins: [
del({ targets: "dist/*" }),
copy({
// clearly this isn't really typescript, so we need to manually copy the type definition file
targets: [
{
src: input.replace(".js", ".d.ts"),
dest: "dist",
rename: pkg.types.replace("./dist/", ""),
},
],
}),
resolve(),
eslint(),
babel({
babelHelpers: "bundled",
presets: [["@babel/preset-env"]],
exclude: ["node_modules/**"],
}),
filesize(),
],
},
{
// modules
input,
output: [
{
// ES6 module (import)
file: pkg.exports.import,
format: "esm",
exports: "named",
banner: banner,
},
{
// commonjs (require)
file: pkg.exports.require,
format: "cjs",
exports: "named",
banner: banner,
},
],
plugins: [
resolve(),
babel({
babelHelpers: "bundled",
exclude: ["node_modules/**"],
}),
filesize(),
],
},
];

42
src/index.d.ts vendored Normal file
View File

@ -0,0 +1,42 @@
import type { Options as NormalizeOptions } from "normalize-url";
export interface Options {
/**
* Clean-up and normalize the determined canonical URL.
*
* @default false
*/
readonly normalize?: boolean;
/**
* Options passed directly to [`normalize-url`](https://github.com/sindresorhus/normalize-url#options).
*
* Requires options.normalize = true.
*
* @default { stripWWW: false, stripHash: true, removeQueryParameters: true, removeTrailingSlash: false }
*/
readonly normalizeOptions?: NormalizeOptions;
/**
* Make an educated guess using other clues if canonical isn't explicitly set in the page's <head>.
*
* @default false
*/
readonly guess?: boolean;
};
/**
* Returns the current page's canonical URL.
*
* @example
* ```
* // This imaginary page's <head> contains the following link tag:
* // <link rel="canonical" href="https://www.example.com/" />
*
* import canonicalUrl from "get-canonical-url";
*
* canonicalUrl();
* //=> 'https://www.example.com/'
* ```
*/
export default function canonicalUrl(options?: Options): string | undefined;

39
src/index.js Normal file
View File

@ -0,0 +1,39 @@
import normalizeUrl from "normalize-url";
export default function canonicalUrl(options) {
options = {
normalize: false,
normalizeOptions: {
// A few sensible normalize-url defaults:
// https://github.com/sindresorhus/normalize-url#options
stripWWW: false,
stripHash: true,
removeQueryParameters: true,
removeTrailingSlash: false,
},
guess: false,
...options,
};
// Start with a blank slate
let url = undefined;
// Look for a <link rel="canonical"> tag in the page's <head>
const linkElement = document.head.querySelector("link[rel='canonical']");
if (linkElement !== null) {
// Easy peasy, there was a <link rel="canonical"> tag!
url = linkElement.href;
} else if (options.guess) {
// We've been told to make an educated guess if canonical isn't explicitly set
url = document.documentURI || document.URL || window.location.href;
}
if (options.normalize) {
// Pass either custom options or defaults (above) directly to normalize-url
url = normalizeUrl(url, options.normalizeOptions);
}
// Some sort of URL has been determined by this point, unless it's impossible
return url;
}

7
test/fixtures/with-canonical-tag.html vendored Normal file
View File

@ -0,0 +1,7 @@
<!doctype html>
<html>
<head>
<link rel="canonical" href="https://test.example.com/this/doesnt/exist.aspx?no=really&it=doesnt#gocheck">
<script src="../../dist/get-canonical-url.min.js"></script>
</head>
</html>

View File

@ -0,0 +1,6 @@
<!doctype html>
<html>
<head>
<script src="../../dist/get-canonical-url.min.js"></script>
</head>
</html>

74
test/index.spec.js Normal file
View File

@ -0,0 +1,74 @@
/* eslint-env mocha */
import path from "path";
import { fileURLToPath } from "url";
import { JSDOM } from "jsdom";
import { expect } from "chai";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
describe("with canonical tag", () => {
beforeEach(async () => {
const domWithTag = await JSDOM.fromFile(path.resolve(__dirname, "fixtures", "with-canonical-tag.html"), {
runScripts: "dangerously",
resources: "usable",
});
// TODO: be better.
await wait(500);
global.window = domWithTag.window;
});
it("all defaults", () => {
expect(window.canonicalUrl())
.to.equal("https://test.example.com/this/doesnt/exist.aspx?no=really&it=doesnt#gocheck");
});
it("normalized (default options)", () => {
expect(window.canonicalUrl({
normalize: true,
})).to.equal("https://test.example.com/this/doesnt/exist.aspx");
});
it("normalized (custom options)", () => {
expect(window.canonicalUrl({
normalize: true,
normalizeOptions: {
stripProtocol: true,
stripHash: true,
},
})).to.equal("test.example.com/this/doesnt/exist.aspx?it=doesnt&no=really");
});
});
describe("without canonical tag", () => {
beforeEach(async () => {
const domWithoutTag = await JSDOM.fromFile(path.resolve(__dirname, "fixtures", "without-canonical-tag.html"), {
runScripts: "dangerously",
resources: "usable",
});
domWithoutTag.reconfigure({ url: "https://test.example.com/this/doesnt/exist.aspx?no=really&it=doesnt#gocheck" });
// TODO: be better.
await wait(500);
global.window = domWithoutTag.window;
});
it("no guess, should give up", () => {
expect(window.canonicalUrl()).to.be.undefined;
});
it("guess from window.location.href", () => {
expect(window.canonicalUrl({
guess: true,
normalize: false,
})).to.equal("https://test.example.com/this/doesnt/exist.aspx?no=really&it=doesnt#gocheck");
});
});
async function wait(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}

4576
yarn.lock Normal file

File diff suppressed because it is too large Load Diff