@haruhimemoe/next-kit 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -1
- package/README.md +4 -4
- package/dist/seo/index.d.ts +1 -1
- package/dist/seo/index.js +1 -1
- package/dist/seo/metadata.d.ts +8 -2
- package/dist/seo/metadata.js +4 -3
- package/dist/seo/site.d.ts +13 -2
- package/dist/seo/site.js +20 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.4.0] - 2026-09-28
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `seo`: a short title suffix. `Site.shortTitleSuffix` (like "pools") and `pageMetadata`'s `titleSuffix` option: `"auto"` (the default) keeps "keyword · host" and switches to "keyword · pools" only when the full title passes 60 characters (`TITLE_MAX`) and the site sets a short suffix; `"full"`, `"short"` and `"none"` force one. `pageTitle` takes the same mode (default `"full"`) and `notFoundMetadata` shortens like `"auto"`. Sites without `shortTitleSuffix` get the same titles as before.
|
|
14
|
+
|
|
9
15
|
## [0.3.0] - 2026-09-28
|
|
10
16
|
|
|
11
17
|
### Added
|
|
@@ -46,7 +52,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
46
52
|
- `@haruhimemoe/next-kit/auth-react`: `createSignedInMarker`, `createAccountStore`, `useAccount`, `createAccount`, `RestoreSignedIn` and `osuSignIn`.
|
|
47
53
|
- `@haruhimemoe/next-kit/testing`: `startMemoryMongo`, `setupTestDb`, `setupMsw` and the fake osu! app env.
|
|
48
54
|
|
|
49
|
-
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.
|
|
55
|
+
[unreleased]: https://github.com/haruhimemoe/next-kit/compare/v0.4.0...HEAD
|
|
56
|
+
[0.4.0]: https://github.com/haruhimemoe/next-kit/compare/v0.3.0...v0.4.0
|
|
50
57
|
[0.3.0]: https://github.com/haruhimemoe/next-kit/compare/v0.2.1...v0.3.0
|
|
51
58
|
[0.2.1]: https://github.com/haruhimemoe/next-kit/compare/v0.2.0...v0.2.1
|
|
52
59
|
[0.2.0]: https://github.com/haruhimemoe/next-kit/compare/v0.1.0...v0.2.0
|
package/README.md
CHANGED
|
@@ -217,16 +217,16 @@ export const GET = () => textResponse(llmsTxt({ title: "pools.haruhime.moe", sum
|
|
|
217
217
|
|
|
218
218
|
### seo
|
|
219
219
|
|
|
220
|
-
Since 0.3.0. Every helper takes the app's `Site`: `name`, `url` (the canonical origin), `title` (the home page's primary keyword), `titleSuffix?` (defaults to the host), `description`, `locale?` (en_US), `twitter?`, `ogImages`, `organization` and `parent?`.
|
|
220
|
+
Since 0.3.0. Every helper takes the app's `Site`: `name`, `url` (the canonical origin), `title` (the home page's primary keyword), `titleSuffix?` (defaults to the host), `shortTitleSuffix?` (since 0.4.0, like "pools"), `description`, `locale?` (en_US), `twitter?`, `ogImages`, `organization` and `parent?`.
|
|
221
221
|
|
|
222
222
|
| Export | What it does |
|
|
223
223
|
| --- | --- |
|
|
224
224
|
| `siteMetadata(site)` | The root layout's `Metadata`: `metadataBase`, title default "keyword · host" and template "%s · host", the clamped description, `applicationName`, openGraph (type, site name, locale, images) and the twitter card. No canonical (a layout canonical leaks into every child) and no icons (the app's icon files). |
|
|
225
225
|
| `homeMetadata(site, { title?, description? })` | `pageMetadata` for `/` with the site's keyword title. |
|
|
226
|
-
| `pageMetadata(site, { path, title, description?, index?, ogType?, images?, modifiedTime?, publishedTime? })` | An absolute "keyword · host" title, the description clamped to 160, `alternates.canonical` and `openGraph.url` set together to the same absolute URL, and a full openGraph and twitter card (images default to `site.ogImages`: Next replaces a layout's openGraph, it doesn't merge it). `index: false` adds noindex, follow. `ogType: "article"` writes the ISO times it has. Throws on a relative path, another origin or a blank title. |
|
|
227
|
-
| `notFoundMetadata(site, what?)` | "Pack not found · host" and noindex, for a `generateMetadata` whose record is missing. |
|
|
226
|
+
| `pageMetadata(site, { path, title, titleSuffix?, description?, index?, ogType?, images?, modifiedTime?, publishedTime? })` | An absolute "keyword · host" title (`titleSuffix` defaults to `"auto"`: "keyword · pools" when the full title passes 60 characters and the site sets `shortTitleSuffix`; `"full"`, `"short"` or `"none"` force one), the description clamped to 160, `alternates.canonical` and `openGraph.url` set together to the same absolute URL, and a full openGraph and twitter card (images default to `site.ogImages`: Next replaces a layout's openGraph, it doesn't merge it). `index: false` adds noindex, follow. `ogType: "article"` writes the ISO times it has. Throws on a relative path, another origin or a blank title. |
|
|
227
|
+
| `notFoundMetadata(site, what?)` | "Pack not found · host" (shortened like `"auto"`) and noindex, for a `generateMetadata` whose record is missing. |
|
|
228
228
|
| `clampDescription(text, max?)`, `DESCRIPTION_MAX` | One line, at most `max` (160) characters: cut at a word, trailing punctuation dropped, "…" added. |
|
|
229
|
-
| `pageTitle(site, title)`, `TITLE_SEPARATOR` | "keyword · host", the suffix added once. |
|
|
229
|
+
| `pageTitle(site, title, mode?)`, `TITLE_SEPARATOR`, `TITLE_MAX`, `TitleSuffixMode` | "keyword · host", the suffix added once (either suffix already there counts). `mode`: `"full"` (default), `"short"` (the full one when the site has none), `"none"`, or `"auto"` (short past `TITLE_MAX`, 60). |
|
|
230
230
|
| `robots(site, { allow?, disallow?, aiBots? })` | `MetadataRoute.Robots`: the `*` group, one group naming the allowed AI bots with the same rules (a bot with its own group ignores `*`), a `Disallow: /` group for blocked ones, the sitemap and the host. `aiBots`: `"allow"` (default), `"block-training"`, or `"block-all"` (Bingbot stays, or the site leaves Bing). |
|
|
231
231
|
| `AI_BOTS` | The named crawlers, each `{ userAgent, operator, kind: "training" \| "search", searchEngine? }`: GPTBot, OAI-SearchBot, ChatGPT-User, PerplexityBot, Perplexity-User, ClaudeBot, Claude-SearchBot, Claude-User, anthropic-ai, Google-Extended, Applebot-Extended, Bingbot, CCBot, Bytespider, meta-externalagent. |
|
|
232
232
|
| `sitemapEntries(site, groups)`, `SITEMAP_MAX_URLS` | `MetadataRoute.Sitemap` from groups of paths and `{ path, lastModified?, changeFrequency?, priority? }` records: absolute URLs on the canonical origin, the first entry per URL, `lastModified` only when it's a real date (never made up). Throws past 50,000 URLs or on a priority outside 0 to 1. |
|
package/dist/seo/index.d.ts
CHANGED
|
@@ -15,5 +15,5 @@ export { type LdGraph, type LdLink, type LdNode, SEARCH_TERM, type WebApplicatio
|
|
|
15
15
|
export { type LlmsFullPart, type LlmsLink, type LlmsSection, type LlmsTxtOptions, llmsFull, llmsTxt, type TextResponseOptions, textResponse, } from "./llms.js";
|
|
16
16
|
export { homeMetadata, notFoundMetadata, type PageMetadataOptions, pageMetadata, siteMetadata, } from "./metadata.js";
|
|
17
17
|
export { AI_BOTS, type AiBot, type AiBotKind, type AiBotsPolicy, type RobotsOptions, robots, } from "./robots.js";
|
|
18
|
-
export { HARUHIME_ORG, type OgImage, type Organization, pageTitle, type Site, TITLE_SEPARATOR, } from "./site.js";
|
|
18
|
+
export { HARUHIME_ORG, type OgImage, type Organization, pageTitle, type Site, TITLE_MAX, TITLE_SEPARATOR, type TitleSuffixMode, } from "./site.js";
|
|
19
19
|
export { type ChangeFrequency, SITEMAP_MAX_URLS, type SitemapGroup, type SitemapRecord, sitemapEntries, } from "./sitemap.js";
|
package/dist/seo/index.js
CHANGED
|
@@ -14,5 +14,5 @@ export { SEARCH_TERM, } from "./ld-site.js";
|
|
|
14
14
|
export { llmsFull, llmsTxt, textResponse, } from "./llms.js";
|
|
15
15
|
export { homeMetadata, notFoundMetadata, pageMetadata, siteMetadata, } from "./metadata.js";
|
|
16
16
|
export { AI_BOTS, robots, } from "./robots.js";
|
|
17
|
-
export { HARUHIME_ORG, pageTitle, TITLE_SEPARATOR, } from "./site.js";
|
|
17
|
+
export { HARUHIME_ORG, pageTitle, TITLE_MAX, TITLE_SEPARATOR, } from "./site.js";
|
|
18
18
|
export { SITEMAP_MAX_URLS, sitemapEntries, } from "./sitemap.js";
|
package/dist/seo/metadata.d.ts
CHANGED
|
@@ -9,13 +9,18 @@
|
|
|
9
9
|
* @modified Mon Sep 28, 2026
|
|
10
10
|
*/
|
|
11
11
|
import type { Metadata } from "next";
|
|
12
|
-
import { type OgImage, type Site } from "./site.js";
|
|
12
|
+
import { type OgImage, type Site, type TitleSuffixMode } from "./site.js";
|
|
13
13
|
/** pageMetadata's input. */
|
|
14
14
|
export type PageMetadataOptions = {
|
|
15
15
|
/** The page's path, like "/search". Canonical and og:url both come from it. */
|
|
16
16
|
path: string;
|
|
17
17
|
/** The primary keyword; " · host" is added (pageTitle). */
|
|
18
18
|
title: string;
|
|
19
|
+
/**
|
|
20
|
+
* Default "auto": the full suffix, or the site's shortTitleSuffix when the full title would
|
|
21
|
+
* pass TITLE_MAX (60). "full", "short" and "none" force one.
|
|
22
|
+
*/
|
|
23
|
+
titleSuffix?: TitleSuffixMode;
|
|
19
24
|
/** Defaults to the site's description. Clamped to 160 characters. */
|
|
20
25
|
description?: string;
|
|
21
26
|
/** false: noindex, follow. Default true. */
|
|
@@ -40,7 +45,8 @@ export declare const siteMetadata: (site: Site) => Metadata;
|
|
|
40
45
|
* @function pageMetadata
|
|
41
46
|
* @param site {Site} the site
|
|
42
47
|
* @param options {PageMetadataOptions} the page
|
|
43
|
-
* @returns {Metadata} an absolute "keyword · host" title
|
|
48
|
+
* @returns {Metadata} an absolute "keyword · host" title (or "keyword · short" past 60
|
|
49
|
+
* characters, see titleSuffix), the clamped description, the
|
|
44
50
|
* canonical and og:url (always together, as absolute URLs), a full openGraph and
|
|
45
51
|
* twitter card, and robots noindex when index is false
|
|
46
52
|
* @throws {Error} when path doesn't start with "/" or the title is blank
|
package/dist/seo/metadata.js
CHANGED
|
@@ -47,14 +47,15 @@ export const siteMetadata = (site) => {
|
|
|
47
47
|
* @function pageMetadata
|
|
48
48
|
* @param site {Site} the site
|
|
49
49
|
* @param options {PageMetadataOptions} the page
|
|
50
|
-
* @returns {Metadata} an absolute "keyword · host" title
|
|
50
|
+
* @returns {Metadata} an absolute "keyword · host" title (or "keyword · short" past 60
|
|
51
|
+
* characters, see titleSuffix), the clamped description, the
|
|
51
52
|
* canonical and og:url (always together, as absolute URLs), a full openGraph and
|
|
52
53
|
* twitter card, and robots noindex when index is false
|
|
53
54
|
* @throws {Error} when path doesn't start with "/" or the title is blank
|
|
54
55
|
*/
|
|
55
56
|
export const pageMetadata = (site, options) => {
|
|
56
57
|
const url = absoluteUrl(site, options.path);
|
|
57
|
-
const title = pageTitle(site, options.title);
|
|
58
|
+
const title = pageTitle(site, options.title, options.titleSuffix ?? "auto");
|
|
58
59
|
const description = clampDescription(options.description ?? site.description);
|
|
59
60
|
const images = [...(options.images ?? site.ogImages)];
|
|
60
61
|
const base = {
|
|
@@ -101,6 +102,6 @@ export const homeMetadata = (site, options = {}) => pageMetadata(site, {
|
|
|
101
102
|
* records returned {} and got the site's default title)
|
|
102
103
|
*/
|
|
103
104
|
export const notFoundMetadata = (site, what = "Page") => ({
|
|
104
|
-
title: { absolute: pageTitle(site, `${what} not found
|
|
105
|
+
title: { absolute: pageTitle(site, `${what} not found`, "auto") },
|
|
105
106
|
robots: { index: false, follow: false },
|
|
106
107
|
});
|
package/dist/seo/site.d.ts
CHANGED
|
@@ -35,6 +35,8 @@ export type Site = {
|
|
|
35
35
|
title: string;
|
|
36
36
|
/** What follows " · " in every title. Defaults to the host, like pools.haruhime.moe. */
|
|
37
37
|
titleSuffix?: string;
|
|
38
|
+
/** A shorter suffix, like "pools", for titles that would pass TITLE_MAX with the full one. */
|
|
39
|
+
shortTitleSuffix?: string;
|
|
38
40
|
/** The default description, 140 to 160 characters. */
|
|
39
41
|
description: string;
|
|
40
42
|
/** Open Graph locale. Defaults to en_US. */
|
|
@@ -79,6 +81,14 @@ export declare const absoluteUrl: (site: Pick<Site, "url">, path: string) => str
|
|
|
79
81
|
* @returns {string} a stable JSON-LD @id, like https://www.haruhime.moe/#organization
|
|
80
82
|
*/
|
|
81
83
|
export declare const nodeId: (url: string, name: string) => string;
|
|
84
|
+
/** The longest title search results show in full; "auto" switches to the short suffix past it. */
|
|
85
|
+
export declare const TITLE_MAX = 60;
|
|
86
|
+
/**
|
|
87
|
+
* Which suffix a title gets: "full" (titleSuffix or the host), "short" (shortTitleSuffix, else
|
|
88
|
+
* full), "none" (the keyword alone), or "auto" (full, unless that passes TITLE_MAX and the site
|
|
89
|
+
* has a shortTitleSuffix).
|
|
90
|
+
*/
|
|
91
|
+
export type TitleSuffixMode = "auto" | "full" | "short" | "none";
|
|
82
92
|
/**
|
|
83
93
|
* @function titleSuffix
|
|
84
94
|
* @param site {Site} the site
|
|
@@ -89,10 +99,11 @@ export declare const titleSuffix: (site: Site) => string;
|
|
|
89
99
|
* @function pageTitle
|
|
90
100
|
* @param site {Site} the site
|
|
91
101
|
* @param title {string} the page's primary keyword, like "Search osu! tournament mappools"
|
|
92
|
-
* @
|
|
102
|
+
* @param mode {TitleSuffixMode} which suffix to add (default "full")
|
|
103
|
+
* @returns {string} "Primary keyword · host" (a suffix already there is never added twice)
|
|
93
104
|
* @throws {Error} when title is blank
|
|
94
105
|
*/
|
|
95
|
-
export declare const pageTitle: (site: Site, title: string) => string;
|
|
106
|
+
export declare const pageTitle: (site: Site, title: string, mode?: TitleSuffixMode) => string;
|
|
96
107
|
/**
|
|
97
108
|
* @function isoDate
|
|
98
109
|
* @param value {string | Date | null | undefined} a date, maybe unknown
|
package/dist/seo/site.js
CHANGED
|
@@ -55,6 +55,8 @@ export const absoluteUrl = (site, path) => {
|
|
|
55
55
|
* @returns {string} a stable JSON-LD @id, like https://www.haruhime.moe/#organization
|
|
56
56
|
*/
|
|
57
57
|
export const nodeId = (url, name) => `${origin(url)}/#${name}`;
|
|
58
|
+
/** The longest title search results show in full; "auto" switches to the short suffix past it. */
|
|
59
|
+
export const TITLE_MAX = 60;
|
|
58
60
|
/**
|
|
59
61
|
* @function titleSuffix
|
|
60
62
|
* @param site {Site} the site
|
|
@@ -65,15 +67,29 @@ export const titleSuffix = (site) => site.titleSuffix ?? new URL(site.url).host;
|
|
|
65
67
|
* @function pageTitle
|
|
66
68
|
* @param site {Site} the site
|
|
67
69
|
* @param title {string} the page's primary keyword, like "Search osu! tournament mappools"
|
|
68
|
-
* @
|
|
70
|
+
* @param mode {TitleSuffixMode} which suffix to add (default "full")
|
|
71
|
+
* @returns {string} "Primary keyword · host" (a suffix already there is never added twice)
|
|
69
72
|
* @throws {Error} when title is blank
|
|
70
73
|
*/
|
|
71
|
-
export const pageTitle = (site, title) => {
|
|
74
|
+
export const pageTitle = (site, title, mode = "full") => {
|
|
72
75
|
const keyword = title.replace(/\s+/g, " ").trim();
|
|
73
76
|
if (!keyword)
|
|
74
77
|
throw new Error("seo: a page title can't be blank");
|
|
75
|
-
const
|
|
76
|
-
|
|
78
|
+
const full = titleSuffix(site);
|
|
79
|
+
const suffixes = [full, site.shortTitleSuffix]
|
|
80
|
+
.filter(Boolean)
|
|
81
|
+
.map((s) => `${TITLE_SEPARATOR}${s}`);
|
|
82
|
+
if (mode === "none" || suffixes.some((suffix) => keyword.endsWith(suffix)))
|
|
83
|
+
return keyword;
|
|
84
|
+
const long = `${keyword}${TITLE_SEPARATOR}${full}`;
|
|
85
|
+
const short = site.shortTitleSuffix
|
|
86
|
+
? `${keyword}${TITLE_SEPARATOR}${site.shortTitleSuffix}`
|
|
87
|
+
: long;
|
|
88
|
+
if (mode === "short")
|
|
89
|
+
return short;
|
|
90
|
+
if (mode === "auto" && long.length > TITLE_MAX)
|
|
91
|
+
return short;
|
|
92
|
+
return long;
|
|
77
93
|
};
|
|
78
94
|
/**
|
|
79
95
|
* @function isoDate
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@haruhimemoe/next-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "The Next.js plumbing the haruhime.moe tools share: JSON route helpers, rate limits and budgets in MongoDB, bearer machine auth, zod env parsing, a connect-once MongoDB client with safe index builds, better-auth with osu! sign-in, the signed-in marker and account store for the browser, and SEO: metadata, robots.txt, sitemaps, JSON-LD and llms.txt.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nextjs",
|