nuxt-seo-utils
Advanced tools
Comparing version
@@ -85,2 +85,8 @@ import * as _nuxt_schema from '@nuxt/schema'; | ||
/** | ||
* When enabled, it will lowercase the canonical URL. | ||
* | ||
* @default true | ||
*/ | ||
canonicalLowercase?: boolean; | ||
/** | ||
* When enabled, it will redirect any request to the canonical domain (site url) using a 301 redirect on non-dev environments. | ||
@@ -87,0 +93,0 @@ * |
@@ -5,6 +5,6 @@ { | ||
"compatibility": { | ||
"nuxt": ">=3.6.1", | ||
"nuxt": ">=3.16.0", | ||
"bridge": false | ||
}, | ||
"version": "6.0.11", | ||
"version": "6.0.12", | ||
"builder": { | ||
@@ -11,0 +11,0 @@ "@nuxt/module-builder": "0.8.4", |
@@ -64,2 +64,8 @@ import type { NuxtLinkProps } from 'nuxt/app'; | ||
hideRoot?: MaybeRefOrGetter<boolean>; | ||
/** | ||
* The root segment of the breadcrumb list. | ||
* | ||
* By default, this will be `/`, unless you're using Nuxt I18n with a prefix strategy. | ||
*/ | ||
rootSegment?: string; | ||
} | ||
@@ -95,3 +101,3 @@ export interface BreadcrumbItemProps extends NuxtUIBreadcrumbItem { | ||
*/ | ||
export declare function useBreadcrumbItems(options?: BreadcrumbProps): import("vue").ComputedRef<BreadcrumbItemProps[]>; | ||
export declare function useBreadcrumbItems(_options?: BreadcrumbProps): import("vue").ComputedRef<BreadcrumbItemProps[]>; | ||
export {}; |
@@ -12,3 +12,3 @@ import { | ||
import { withoutTrailingSlash } from "ufo"; | ||
import { computed, toValue } from "vue"; | ||
import { computed, inject, onUnmounted, getCurrentInstance, provide, ref, toRaw, toValue } from "vue"; | ||
import { pathBreadcrumbSegments } from "../../shared/breadcrumbs.js"; | ||
@@ -21,3 +21,29 @@ function withoutQuery(path) { | ||
} | ||
export function useBreadcrumbItems(options = {}) { | ||
const BreadcrumbCtx = Symbol("BreadcrumbCtx"); | ||
export function useBreadcrumbItems(_options = {}) { | ||
const vm = getCurrentInstance(); | ||
let isCreatingState = false; | ||
if (vm) { | ||
let stateRef2 = inject(BreadcrumbCtx, null); | ||
if (!stateRef2) { | ||
stateRef2 = ref({}); | ||
provide(BreadcrumbCtx, stateRef2); | ||
isCreatingState = false; | ||
} | ||
const id2 = "breadcrumb"; | ||
const state = stateRef2.value; | ||
if (!state[id2]) { | ||
state[id2] = []; | ||
} | ||
const idx = state[id2].push(_options) - 1; | ||
stateRef2.value = state; | ||
onUnmounted(() => { | ||
stateRef2.value = Object.fromEntries(Object.entries(stateRef2.value).map(([k, v]) => { | ||
if (k === id2) { | ||
return v.filter((_, i) => i !== idx); | ||
} | ||
return v; | ||
})); | ||
}); | ||
} | ||
const router = useRouter(); | ||
@@ -31,9 +57,25 @@ const i18n = useI18n(); | ||
const items = computed(() => { | ||
let rootNode = "/"; | ||
const optionStack = stateRef.value?.[id]; | ||
const flatOptions = toRaw([...optionStack]).reduce((acc, cur) => { | ||
acc.rootSegment = acc.rootSegment || cur.rootSegment; | ||
acc.path = acc.path || cur.path; | ||
return acc; | ||
}, {}); | ||
let rootNode = flatOptions.rootSegment || "/"; | ||
if (i18n) { | ||
if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && toValue(i18n.defaultLocale) !== toValue(i18n.locale)) | ||
rootNode = `/${toValue(i18n.locale)}`; | ||
rootNode = `${rootNode}${toValue(i18n.locale)}`; | ||
} | ||
const current = withoutQuery(withoutTrailingSlash(toValue(options.path || router.currentRoute.value?.path) || rootNode)); | ||
const overrides = toValue(options.overrides) || []; | ||
const current = withoutQuery(withoutTrailingSlash(toValue(flatOptions.path || router.currentRoute.value?.path) || rootNode)); | ||
const allOverrides = toRaw([...optionStack])?.map((opts) => toValue(opts.overrides)).filter(Boolean); | ||
const flatOverrides = allOverrides?.reduce((acc, i) => { | ||
if (i) { | ||
i.forEach((item, index) => { | ||
if (item !== void 0) { | ||
acc[index] = item; | ||
} | ||
}); | ||
} | ||
return acc; | ||
}, []) || {}; | ||
const segments = pathBreadcrumbSegments(current, rootNode).map((path, index) => { | ||
@@ -43,17 +85,19 @@ let item = { | ||
}; | ||
if (typeof overrides[index] !== "undefined") { | ||
if (overrides[index] === false) | ||
if (typeof flatOverrides[index] !== "undefined") { | ||
if (flatOverrides[index] === false) | ||
return false; | ||
item = defu(overrides[index], item); | ||
item = defu(flatOverrides[index], item); | ||
} | ||
return item; | ||
}); | ||
if (options.prepend) | ||
segments.unshift(...toValue(options.prepend)); | ||
if (options.append) | ||
segments.push(...toValue(options.append)); | ||
const allPrepends = toRaw([...optionStack]).flatMap((opts) => toValue(opts.prepend)).filter(Boolean); | ||
const allAppends = toRaw([...optionStack]).flatMap((opts) => toValue(opts.append)).filter(Boolean); | ||
if (allPrepends.length) | ||
segments.unshift(...allPrepends); | ||
if (allAppends.length) | ||
segments.push(...allAppends); | ||
return segments.filter(Boolean).map((item) => { | ||
let fallbackLabel = titleCase(String((item.to || "").split("/").pop())); | ||
let fallbackAriaLabel = ""; | ||
const route = router.resolve(item.to)?.matched?.[0]; | ||
const route = item.to ? router.resolve(item.to)?.matched?.[0] : null; | ||
if (route) { | ||
@@ -72,3 +116,3 @@ const routeMeta = route?.meta || {}; | ||
fallbackLabel = routeMeta.breadcrumbLabel || routeMeta.breadcrumbTitle || routeMeta.title || fallbackLabel; | ||
fallbackLabel = i18n.t(`breadcrumb.items.${routeName}.label`, fallbackLabel, { missingWarn: true }); | ||
fallbackLabel = i18n.t(`breadcrumb.items.${routeName}.label`, fallbackLabel, { missingWarn: false }); | ||
fallbackAriaLabel = i18n.t(`breadcrumb.items.${routeName}.ariaLabel`, fallbackAriaLabel, { missingWarn: false }); | ||
@@ -79,3 +123,3 @@ } | ||
item.current = item.current || item.to === current; | ||
if (toValue(options.hideCurrent) && item.current) | ||
if (toValue(flatOptions.hideCurrent) && item.current) | ||
return false; | ||
@@ -86,3 +130,3 @@ return item; | ||
m.to = fixSlashes(siteConfig.trailingSlash, m.to); | ||
if (m.to === rootNode && toValue(options.hideRoot)) | ||
if (m.to === rootNode && toValue(flatOptions.hideRoot)) | ||
return false; | ||
@@ -93,7 +137,7 @@ } | ||
}); | ||
const schemaOrgEnabled = typeof options.schemaOrg === "undefined" ? true : options.schemaOrg; | ||
if ((import.meta.dev || import.meta.server) && schemaOrgEnabled) { | ||
const schemaOrgEnabled = typeof _options.schemaOrg === "undefined" ? true : _options.schemaOrg; | ||
if (isCreatingState && (import.meta.dev || import.meta.server || import.meta.env?.NODE_ENV === "test") && schemaOrgEnabled) { | ||
useSchemaOrg([ | ||
defineBreadcrumb({ | ||
id: `#${options.id || "breadcrumb"}`, | ||
id: `#${_options.id || "breadcrumb"}`, | ||
itemListElement: computed(() => items.value.map((item) => ({ | ||
@@ -100,0 +144,0 @@ name: item.label || item.ariaLabel, |
@@ -0,4 +1,4 @@ | ||
import { useHead, useSeoMeta } from "#imports"; | ||
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig"; | ||
import { createSitePathResolver } from "#site-config/app/composables/utils"; | ||
import { useHead, useSeoMeta } from "@unhead/vue"; | ||
import { useError, useRoute, useRuntimeConfig } from "nuxt/app"; | ||
@@ -8,3 +8,3 @@ import { stringifyQuery } from "ufo"; | ||
export function applyDefaults(i18n) { | ||
const { canonicalQueryWhitelist } = useRuntimeConfig().public["seo-utils"]; | ||
const { canonicalQueryWhitelist, canonicalLowercase } = useRuntimeConfig().public["seo-utils"]; | ||
const siteConfig = useSiteConfig(); | ||
@@ -19,5 +19,9 @@ const route = useRoute(); | ||
const { query } = route; | ||
const url = resolveUrl(route.path || "/").value || route.path; | ||
let url = resolveUrl(route.path || "/").value || route.path; | ||
if (canonicalLowercase) { | ||
url = url.toLocaleLowerCase(siteConfig.currentLocale); | ||
} | ||
const filteredQuery = Object.fromEntries( | ||
Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key)) | ||
Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b)) | ||
// Sort params | ||
); | ||
@@ -34,2 +38,3 @@ return Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url; | ||
} | ||
useHead({ templateParams }); | ||
const minimalPriority = { | ||
@@ -36,0 +41,0 @@ // give nuxt.config values higher priority |
@@ -1,3 +0,3 @@ | ||
import { unpackMeta } from "@unhead/shared"; | ||
import { injectHead } from "@unhead/vue"; | ||
import { injectHead } from "#imports"; | ||
import { unpackMeta } from "@unhead/vue/utils"; | ||
import { defineNuxtPlugin, useRequestEvent } from "nuxt/app"; | ||
@@ -4,0 +4,0 @@ export default defineNuxtPlugin({ |
@@ -0,3 +1,3 @@ | ||
import { injectHead } from "#imports"; | ||
import { createSitePathResolver } from "#site-config/app/composables/utils"; | ||
import { injectHead } from "@unhead/vue"; | ||
import { defineNuxtPlugin } from "nuxt/app"; | ||
@@ -17,2 +17,3 @@ import { unref } from "vue"; | ||
head.use({ | ||
key: "absoluteImageUrls", | ||
hooks: { | ||
@@ -19,0 +20,0 @@ "tags:resolve": async ({ tags }) => { |
@@ -0,3 +1,3 @@ | ||
import { injectHead } from "#imports"; | ||
import { InferSeoMetaPlugin } from "@unhead/addons"; | ||
import { injectHead } from "@unhead/vue"; | ||
import { defineNuxtPlugin } from "nuxt/app"; | ||
@@ -4,0 +4,0 @@ export default defineNuxtPlugin(() => { |
@@ -0,3 +1,3 @@ | ||
import { injectHead } from "#imports"; | ||
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig"; | ||
import { injectHead } from "@unhead/vue"; | ||
import { defineNuxtPlugin } from "nuxt/app"; | ||
@@ -27,7 +27,8 @@ export default defineNuxtPlugin(() => { | ||
name: "description", | ||
content: "%site.description" | ||
content: "%site.description", | ||
tagPriority: "low" | ||
} | ||
); | ||
} | ||
head.push(input, { tagPriority: 150 }); | ||
head.push(input); | ||
}); |
@@ -1,2 +0,2 @@ | ||
import { useHead } from "@unhead/vue"; | ||
import { useHead } from "#imports"; | ||
import { defineNuxtPlugin, useError, useRoute } from "nuxt/app"; | ||
@@ -3,0 +3,0 @@ import { titleCase } from "scule"; |
{ | ||
"extends": "../../../.nuxt/tsconfig.server.json" | ||
"extends": "../../../../.nuxt/tsconfig.server.json" | ||
} |
@@ -1,1 +0,1 @@ | ||
export type { Head, UseSeoMetaInput } from '@unhead/schema'; | ||
export type { Head, UseSeoMetaInput } from '@unhead/vue'; |
@@ -0,0 +0,0 @@ MIT License |
{ | ||
"name": "nuxt-seo-utils", | ||
"type": "module", | ||
"version": "6.0.12", | ||
"version": "7.0.0", | ||
"description": "SEO utilities to improve your Nuxt sites discoverability and shareability", | ||
@@ -32,8 +32,12 @@ "license": "MIT", | ||
"@unhead/schema", | ||
"unhead" | ||
"unhead", | ||
"@unhead/vue/utils" | ||
] | ||
}, | ||
"peerDependencies": { | ||
"@unhead/vue": "^1 || ^2" | ||
}, | ||
"dependencies": { | ||
"@nuxt/kit": "^3.15.4", | ||
"@unhead/addons": "^1.11.19", | ||
"@nuxt/kit": "^3.16.0", | ||
"@unhead/addons": "2.0.0-rc.9", | ||
"defu": "^6.1.4", | ||
@@ -43,17 +47,19 @@ "escape-string-regexp": "^5.0.0", | ||
"image-size": "^1.2.0", | ||
"mlly": "^1.7.4", | ||
"nuxt-site-config": "^3.1.0", | ||
"nuxt-site-config": "^3.1.4", | ||
"pathe": "^2.0.3", | ||
"pkg-types": "^2.1.0", | ||
"scule": "^1.3.0", | ||
"semver": "^7.7.1", | ||
"ufo": "^1.5.4" | ||
}, | ||
"devDependencies": { | ||
"@antfu/eslint-config": "4.3.0", | ||
"@antfu/eslint-config": "4.8.1", | ||
"@nuxt/module-builder": "^0.8.4", | ||
"@nuxt/test-utils": "^3.15.4", | ||
"@nuxt/ui": "^2.21.0", | ||
"@nuxt/test-utils": "^3.17.2", | ||
"@nuxt/ui": "^2.21.1", | ||
"@nuxtjs/eslint-config-typescript": "^12.1.0", | ||
"@nuxtjs/i18n": "^9.2.0", | ||
"@unhead/schema": "^1.11.19", | ||
"@unhead/shared": "^1.11.19", | ||
"@nuxtjs/i18n": "^9.3.0", | ||
"@unhead/schema": "^1.11.20", | ||
"@unhead/shared": "^1.11.20", | ||
"@unhead/vue": "2.0.0-rc.9", | ||
"@vue/test-utils": "^2.4.6", | ||
@@ -63,11 +69,11 @@ "bumpp": "^10.0.3", | ||
"cheerio": "1.0.0", | ||
"eslint": "^9.20.1", | ||
"happy-dom": "^17.1.1", | ||
"eslint": "^9.22.0", | ||
"happy-dom": "^17.4.2", | ||
"markdownlint-cli": "^0.44.0", | ||
"nuxt": "^3.15.4", | ||
"nuxt": "^3.16.0", | ||
"nuxt-content-twoslash": "^0.1.2", | ||
"playwright-core": "^1.50.1", | ||
"sass": "^1.85.0", | ||
"typescript": "5.6.3", | ||
"vitest": "^3.0.6" | ||
"playwright-core": "^1.51.0", | ||
"sass": "^1.85.1", | ||
"typescript": "5.8.2", | ||
"vitest": "^3.0.8" | ||
}, | ||
@@ -74,0 +80,0 @@ "resolutions": { |
@@ -0,0 +0,0 @@ <h1>nuxt-seo-utils</h1> |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
55049
7.02%1265
7.2%13
18.18%22
4.76%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated
Updated