nuxt-seo-utils 7.0.15 → 7.0.17
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/dist/module.d.mts +115 -0
- package/dist/module.json +12 -0
- package/dist/module.mjs +562 -0
- package/dist/runtime/app/composables/polyfills.d.ts +11 -0
- package/dist/runtime/app/composables/polyfills.js +27 -0
- package/dist/runtime/app/composables/useBreadcrumbItems.d.ts +112 -0
- package/dist/runtime/app/composables/useBreadcrumbItems.js +193 -0
- package/dist/runtime/app/logic/applyDefaults.d.ts +1 -0
- package/dist/runtime/app/logic/applyDefaults.js +78 -0
- package/dist/runtime/app/plugins/0.routeRules.d.ts +2 -0
- package/dist/runtime/app/plugins/0.routeRules.js +24 -0
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.d.ts +2 -0
- package/dist/runtime/app/plugins/1.absoluteImageUrls.server.js +33 -0
- package/dist/runtime/app/plugins/defaults.d.ts +2 -0
- package/dist/runtime/app/plugins/defaults.js +12 -0
- package/dist/runtime/app/plugins/defaultsWaitI18n.d.ts +2 -0
- package/dist/runtime/app/plugins/defaultsWaitI18n.js +13 -0
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.d.ts +2 -0
- package/dist/runtime/app/plugins/inferSeoMetaPlugin.js +11 -0
- package/dist/runtime/app/plugins/siteConfig.d.ts +2 -0
- package/dist/runtime/app/plugins/siteConfig.js +33 -0
- package/dist/runtime/app/plugins/titles.d.ts +2 -0
- package/dist/runtime/app/plugins/titles.js +30 -0
- package/dist/runtime/server/middleware/redirectCanonical.d.ts +2 -0
- package/dist/runtime/server/middleware/redirectCanonical.js +28 -0
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.d.ts +2 -0
- package/dist/runtime/server/middleware/resolveImagesInPagesDir.js +16 -0
- package/dist/runtime/server/tsconfig.json +3 -0
- package/dist/runtime/shared/breadcrumbs.d.ts +1 -0
- package/dist/runtime/shared/breadcrumbs.js +18 -0
- package/dist/runtime/types.d.ts +6 -0
- package/dist/runtime/types.js +0 -0
- package/dist/types.d.mts +3 -0
- package/package.json +2 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
2
|
+
import { computed, toValue } from "vue";
|
|
3
|
+
export function useSchemaOrg() {
|
|
4
|
+
}
|
|
5
|
+
export function defineWebSite() {
|
|
6
|
+
}
|
|
7
|
+
export function defineWebPage() {
|
|
8
|
+
}
|
|
9
|
+
export function defineBreadcrumb() {
|
|
10
|
+
}
|
|
11
|
+
export function useI18n() {
|
|
12
|
+
const siteConfig = useSiteConfig({
|
|
13
|
+
resolveRefs: false
|
|
14
|
+
});
|
|
15
|
+
return {
|
|
16
|
+
// eslint-disable-next-line unused-imports/no-unused-vars
|
|
17
|
+
t: (_, fallback, options) => fallback,
|
|
18
|
+
te: (_) => false,
|
|
19
|
+
strategy: "no_prefix",
|
|
20
|
+
defaultLocale: computed(() => {
|
|
21
|
+
return toValue(siteConfig.defaultLocale) || "en";
|
|
22
|
+
}),
|
|
23
|
+
locale: computed(() => {
|
|
24
|
+
return toValue(siteConfig.currentLocale) || toValue(siteConfig.defaultLocale) || "en";
|
|
25
|
+
})
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { NuxtLinkProps } from 'nuxt/app';
|
|
2
|
+
import type { MaybeRefOrGetter } from 'vue';
|
|
3
|
+
interface NuxtUIBreadcrumbItem extends NuxtLinkProps {
|
|
4
|
+
label: string;
|
|
5
|
+
labelClass?: string;
|
|
6
|
+
icon?: string;
|
|
7
|
+
iconClass?: string;
|
|
8
|
+
as?: string;
|
|
9
|
+
type?: 'button' | 'submit' | 'reset';
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
active?: boolean;
|
|
12
|
+
exact?: boolean;
|
|
13
|
+
exactQuery?: boolean;
|
|
14
|
+
exactMatch?: boolean;
|
|
15
|
+
inactiveClass?: string;
|
|
16
|
+
[key: string]: any;
|
|
17
|
+
}
|
|
18
|
+
export interface BreadcrumbProps {
|
|
19
|
+
/**
|
|
20
|
+
* Generate the breadcrumbs based on a different path than the current route.
|
|
21
|
+
*/
|
|
22
|
+
path?: MaybeRefOrGetter<string>;
|
|
23
|
+
/**
|
|
24
|
+
* The id of the breadcrumb list. It's recommended to provide a unique
|
|
25
|
+
* id when adding multiple breadcrumb lists to the same page.
|
|
26
|
+
*/
|
|
27
|
+
id?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Append additional breadcrumb items to the end of the list. This is applied
|
|
30
|
+
* after the `overrides` option.
|
|
31
|
+
*/
|
|
32
|
+
append?: MaybeRefOrGetter<BreadcrumbItemProps[]>;
|
|
33
|
+
/**
|
|
34
|
+
* Prepend additional breadcrumb items to the start of the list. This is applied
|
|
35
|
+
* after the `overrides` option.
|
|
36
|
+
*/
|
|
37
|
+
prepend?: MaybeRefOrGetter<BreadcrumbItemProps[]>;
|
|
38
|
+
/**
|
|
39
|
+
* Override any of the breadcrumb items based on the index.
|
|
40
|
+
*/
|
|
41
|
+
overrides?: MaybeRefOrGetter<(BreadcrumbItemProps | false | undefined)[]>;
|
|
42
|
+
/**
|
|
43
|
+
* Should the schema.org breadcrumb be generated.
|
|
44
|
+
* @default true
|
|
45
|
+
*/
|
|
46
|
+
schemaOrg?: boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The Aria Label for the breadcrumbs.
|
|
49
|
+
* You shouldn't need to change this.
|
|
50
|
+
*
|
|
51
|
+
* @default 'Breadcrumbs'
|
|
52
|
+
*/
|
|
53
|
+
ariaLabel?: string;
|
|
54
|
+
/**
|
|
55
|
+
* Should the current breadcrumb item be shown.
|
|
56
|
+
*
|
|
57
|
+
* @default false
|
|
58
|
+
*/
|
|
59
|
+
hideCurrent?: MaybeRefOrGetter<boolean>;
|
|
60
|
+
/**
|
|
61
|
+
* Should the root breadcrumb be shown.
|
|
62
|
+
*/
|
|
63
|
+
hideRoot?: MaybeRefOrGetter<boolean>;
|
|
64
|
+
/**
|
|
65
|
+
* Should breadcrumb items with non-existing path be shown.
|
|
66
|
+
*
|
|
67
|
+
* By default, every path segments will be present in breadcrumb list even if there is no corresponding page for such segment.
|
|
68
|
+
*
|
|
69
|
+
* @default false
|
|
70
|
+
*/
|
|
71
|
+
hideNonExisting?: MaybeRefOrGetter<boolean>;
|
|
72
|
+
/**
|
|
73
|
+
* The root segment of the breadcrumb list.
|
|
74
|
+
*
|
|
75
|
+
* By default, this will be `/`, unless you're using Nuxt I18n with a prefix strategy.
|
|
76
|
+
*/
|
|
77
|
+
rootSegment?: string;
|
|
78
|
+
}
|
|
79
|
+
export type ResolvedBreadcrumbProps = {
|
|
80
|
+
[key in keyof BreadcrumbProps]: BreadcrumbProps[key] extends MaybeRefOrGetter<infer T> ? T : BreadcrumbProps[key];
|
|
81
|
+
};
|
|
82
|
+
export interface BreadcrumbItemProps extends NuxtUIBreadcrumbItem {
|
|
83
|
+
/** Whether the breadcrumb item represents the aria-current. */
|
|
84
|
+
current?: boolean;
|
|
85
|
+
/**
|
|
86
|
+
* The type of current location the breadcrumb item represents, if `isCurrent` is true.
|
|
87
|
+
* @default 'page'
|
|
88
|
+
*/
|
|
89
|
+
ariaCurrent?: 'page' | 'step' | 'location' | 'date' | 'time' | boolean | 'true' | 'false';
|
|
90
|
+
to?: string;
|
|
91
|
+
ariaLabel?: string;
|
|
92
|
+
separator?: boolean | string;
|
|
93
|
+
class?: (string | string[] | undefined)[] | string;
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
_props?: {
|
|
98
|
+
first: boolean;
|
|
99
|
+
last: boolean;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Generate an automatic breadcrumb list that helps users to navigate between pages.
|
|
104
|
+
*
|
|
105
|
+
* - Integrates with [Nuxt Schema.org](/schema-org/getting-started/installation) to generate [BreadcrumbList](https://schema.org/BreadcrumbList) structured data.
|
|
106
|
+
* - Integrates with [Nuxt I18n](https://i18n.nuxtjs.org/) to generate localized breadcrumbs.
|
|
107
|
+
*
|
|
108
|
+
* @see https://github.com/harlan-zw/nuxt-seo/blob/main/src/runtime/nuxt/composables/useBreadcrumbItems.ts
|
|
109
|
+
* @docs https://nuxtseo.com/nuxt-seo/api/breadcrumbs
|
|
110
|
+
*/
|
|
111
|
+
export declare function useBreadcrumbItems(_options?: BreadcrumbProps): import("vue").ComputedRef<BreadcrumbItemProps[]>;
|
|
112
|
+
export {};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { defineBreadcrumb, useI18n, useSchemaOrg } from "#imports";
|
|
2
|
+
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
3
|
+
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
4
|
+
import { defu } from "defu";
|
|
5
|
+
import { fixSlashes } from "nuxt-site-config/urls";
|
|
6
|
+
import { useNuxtApp, useRoute, useRouter, useState } from "nuxt/app";
|
|
7
|
+
import { withoutTrailingSlash } from "ufo";
|
|
8
|
+
import {
|
|
9
|
+
computed,
|
|
10
|
+
getCurrentInstance,
|
|
11
|
+
inject,
|
|
12
|
+
onScopeDispose,
|
|
13
|
+
onUnmounted,
|
|
14
|
+
provide,
|
|
15
|
+
ref,
|
|
16
|
+
toRaw,
|
|
17
|
+
toValue,
|
|
18
|
+
watch
|
|
19
|
+
} from "vue";
|
|
20
|
+
import { pathBreadcrumbSegments } from "../../shared/breadcrumbs.js";
|
|
21
|
+
function withoutQuery(path) {
|
|
22
|
+
return path.split("?")[0];
|
|
23
|
+
}
|
|
24
|
+
function titleCase(s) {
|
|
25
|
+
return s.replaceAll("-", " ").replace(/\w\S*/g, (w) => w.charAt(0).toUpperCase() + w.substr(1).toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
const BreadcrumbCtx = Symbol("BreadcrumbCtx");
|
|
28
|
+
export function useBreadcrumbItems(_options = {}) {
|
|
29
|
+
const nuxtApp = useNuxtApp();
|
|
30
|
+
const vm = getCurrentInstance();
|
|
31
|
+
const id = `${_options.id || "breadcrumb"}`;
|
|
32
|
+
const uid = `${vm?.uid || 0}`;
|
|
33
|
+
const parentChain = [];
|
|
34
|
+
let parent = vm?.parent;
|
|
35
|
+
while (parent) {
|
|
36
|
+
parentChain.push(parent.uid || 0);
|
|
37
|
+
parent = parent.parent;
|
|
38
|
+
}
|
|
39
|
+
const pauseUpdates = ref(import.meta.client && nuxtApp.isHydrating);
|
|
40
|
+
let stateRef = null;
|
|
41
|
+
if (vm) {
|
|
42
|
+
stateRef = inject(BreadcrumbCtx, null);
|
|
43
|
+
if (!stateRef) {
|
|
44
|
+
stateRef = ref({});
|
|
45
|
+
provide(BreadcrumbCtx, stateRef);
|
|
46
|
+
}
|
|
47
|
+
const state = stateRef.value;
|
|
48
|
+
state[uid] = _options;
|
|
49
|
+
stateRef.value = state;
|
|
50
|
+
onUnmounted(() => {
|
|
51
|
+
const state2 = toRaw(stateRef.value);
|
|
52
|
+
delete state2[uid];
|
|
53
|
+
stateRef.value = state2;
|
|
54
|
+
});
|
|
55
|
+
if (import.meta.client) {
|
|
56
|
+
const _ = [];
|
|
57
|
+
_.push(nuxtApp.hooks.hook("page:start", () => {
|
|
58
|
+
pauseUpdates.value = true;
|
|
59
|
+
}));
|
|
60
|
+
_.push(nuxtApp.hooks.hook("page:finish", () => {
|
|
61
|
+
if (!nuxtApp.isHydrating) {
|
|
62
|
+
pauseUpdates.value = false;
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
65
|
+
_.push(nuxtApp.hooks.hook("app:error", () => {
|
|
66
|
+
pauseUpdates.value = false;
|
|
67
|
+
}));
|
|
68
|
+
_.push(nuxtApp.hooks.hook("app:suspense:resolve", () => {
|
|
69
|
+
pauseUpdates.value = false;
|
|
70
|
+
}));
|
|
71
|
+
onScopeDispose(() => {
|
|
72
|
+
_.forEach((hook) => hook?.());
|
|
73
|
+
_.length = 0;
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
const route = useRoute();
|
|
78
|
+
const router = useRouter();
|
|
79
|
+
const i18n = useI18n();
|
|
80
|
+
const siteResolver = createSitePathResolver({
|
|
81
|
+
canonical: true,
|
|
82
|
+
absolute: true
|
|
83
|
+
});
|
|
84
|
+
const lastBreadcrumbs = useState(`nuxt-seo:breadcrumb:${id}`, () => []);
|
|
85
|
+
const siteConfig = useSiteConfig();
|
|
86
|
+
const items = computed(() => {
|
|
87
|
+
if (import.meta.client && pauseUpdates.value) {
|
|
88
|
+
return lastBreadcrumbs.value;
|
|
89
|
+
}
|
|
90
|
+
const state = toValue(stateRef) || {};
|
|
91
|
+
const optionStack = [...parentChain, uid].map((parentId) => state[parentId]).filter(Boolean);
|
|
92
|
+
const flatOptions = optionStack.reduce((acc, _cur) => {
|
|
93
|
+
const cur = {};
|
|
94
|
+
Object.entries(_cur).forEach(([key, value]) => {
|
|
95
|
+
cur[key] = toValue(value);
|
|
96
|
+
});
|
|
97
|
+
acc.hideRoot = typeof cur.hideRoot === "undefined" ? acc.hideRoot : cur.hideRoot;
|
|
98
|
+
acc.hideCurrent = typeof cur.hideCurrent === "undefined" ? acc.hideCurrent : cur.hideCurrent;
|
|
99
|
+
acc.hideNonExisting = typeof cur.hideNonExisting === "undefined" ? acc.hideNonExisting : cur.hideNonExisting;
|
|
100
|
+
acc.rootSegment = acc.rootSegment || cur.rootSegment;
|
|
101
|
+
acc.path = acc.path || cur.path;
|
|
102
|
+
acc.overrides = acc.overrides || [];
|
|
103
|
+
const overrides = toRaw(toValue(cur.overrides));
|
|
104
|
+
if (overrides) {
|
|
105
|
+
overrides.forEach((item, index) => {
|
|
106
|
+
if (item !== void 0) {
|
|
107
|
+
acc.overrides[index] = toRaw(toValue(item));
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const prepend = toRaw(toValue(cur.prepend));
|
|
112
|
+
const append = toRaw(toValue(cur.append));
|
|
113
|
+
if (prepend && prepend.length) {
|
|
114
|
+
acc.prepend = [...acc.prepend || [], ...prepend.map((m) => toRaw(toValue(m)))].filter(Boolean);
|
|
115
|
+
}
|
|
116
|
+
if (append && append.length)
|
|
117
|
+
acc.append = [...acc.append || [], ...append.map((m) => toRaw(toValue(m)))].filter(Boolean);
|
|
118
|
+
return acc;
|
|
119
|
+
}, {});
|
|
120
|
+
let rootNode = flatOptions.rootSegment || "/";
|
|
121
|
+
if (i18n) {
|
|
122
|
+
if (i18n.strategy === "prefix" || i18n.strategy !== "no_prefix" && toValue(i18n.defaultLocale) !== toValue(i18n.locale))
|
|
123
|
+
rootNode = `${rootNode}${toValue(i18n.locale)}`;
|
|
124
|
+
}
|
|
125
|
+
const current = withoutQuery(withoutTrailingSlash(flatOptions.path || toRaw(route)?.path || rootNode)) || "";
|
|
126
|
+
const segments = pathBreadcrumbSegments(current, rootNode).map((path, index) => {
|
|
127
|
+
let item = {
|
|
128
|
+
to: path
|
|
129
|
+
};
|
|
130
|
+
const override = flatOptions.overrides?.[index];
|
|
131
|
+
if (typeof override !== "undefined") {
|
|
132
|
+
if (override === false)
|
|
133
|
+
return false;
|
|
134
|
+
item = defu(override, item);
|
|
135
|
+
}
|
|
136
|
+
return item;
|
|
137
|
+
});
|
|
138
|
+
segments.unshift(...flatOptions.prepend || []);
|
|
139
|
+
segments.push(...flatOptions.append || []);
|
|
140
|
+
return segments.filter(Boolean).map((item) => {
|
|
141
|
+
let fallbackLabel = titleCase(String((item.to || "").split("/").pop()));
|
|
142
|
+
let fallbackAriaLabel = "";
|
|
143
|
+
const route2 = item.to ? router.resolve(item.to)?.matched?.at(-1) : null;
|
|
144
|
+
if (route2) {
|
|
145
|
+
const routeMeta = route2?.meta || {};
|
|
146
|
+
if (routeMeta.breadcrumb) {
|
|
147
|
+
item = defu(item, routeMeta.breadcrumb);
|
|
148
|
+
}
|
|
149
|
+
const routeName = String(route2.name).split("___")?.[0];
|
|
150
|
+
if (routeName === "index") {
|
|
151
|
+
fallbackLabel = "Home";
|
|
152
|
+
}
|
|
153
|
+
fallbackLabel = routeMeta.breadcrumbLabel || routeMeta.breadcrumbTitle || routeMeta.title || fallbackLabel;
|
|
154
|
+
fallbackLabel = i18n.t(`breadcrumb.items.${routeName}.label`, fallbackLabel, { missingWarn: false });
|
|
155
|
+
fallbackAriaLabel = i18n.t(`breadcrumb.items.${routeName}.ariaLabel`, fallbackAriaLabel, { missingWarn: false });
|
|
156
|
+
} else if (flatOptions.hideNonExisting) {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
item.label = item.label || fallbackLabel;
|
|
160
|
+
item.ariaLabel = item.ariaLabel || fallbackAriaLabel || item.label;
|
|
161
|
+
item.current = item.current || item.to === current;
|
|
162
|
+
if (flatOptions.hideCurrent && item.current)
|
|
163
|
+
return false;
|
|
164
|
+
return item;
|
|
165
|
+
}).map((m) => {
|
|
166
|
+
if (m && m.to) {
|
|
167
|
+
m.to = fixSlashes(siteConfig.trailingSlash, m.to);
|
|
168
|
+
if (m.to === rootNode && flatOptions.hideRoot)
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
return m;
|
|
172
|
+
}).filter(Boolean);
|
|
173
|
+
});
|
|
174
|
+
watch(items, (newItems) => {
|
|
175
|
+
if (!pauseUpdates.value) {
|
|
176
|
+
lastBreadcrumbs.value = newItems;
|
|
177
|
+
}
|
|
178
|
+
}, { immediate: true, flush: "sync" });
|
|
179
|
+
const schemaOrgEnabled = typeof _options.schemaOrg === "undefined" ? true : _options.schemaOrg;
|
|
180
|
+
if ((import.meta.dev || import.meta.server || import.meta.env?.NODE_ENV === "test") && schemaOrgEnabled) {
|
|
181
|
+
useSchemaOrg([
|
|
182
|
+
// @ts-expect-error untyped
|
|
183
|
+
defineBreadcrumb({
|
|
184
|
+
id: `#${id}`,
|
|
185
|
+
itemListElement: computed(() => items.value.map((item) => ({
|
|
186
|
+
name: item.label || item.ariaLabel,
|
|
187
|
+
item: item.to ? siteResolver(item.to) : void 0
|
|
188
|
+
})))
|
|
189
|
+
})
|
|
190
|
+
]);
|
|
191
|
+
}
|
|
192
|
+
return items;
|
|
193
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function applyDefaults(): void;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { injectHead, useHead, useSeoMeta } from "#imports";
|
|
2
|
+
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
3
|
+
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
4
|
+
import { TemplateParamsPlugin } from "@unhead/vue/plugins";
|
|
5
|
+
import { useError, useRoute, useRuntimeConfig } from "nuxt/app";
|
|
6
|
+
import { stringifyQuery } from "ufo";
|
|
7
|
+
import { computed, toValue } from "vue";
|
|
8
|
+
export function applyDefaults() {
|
|
9
|
+
const siteConfig = useSiteConfig({
|
|
10
|
+
resolveRefs: false
|
|
11
|
+
});
|
|
12
|
+
const resolveCurrentLocale = () => toValue(siteConfig.currentLocale) || toValue(siteConfig.defaultLocale) || "en";
|
|
13
|
+
const head = injectHead();
|
|
14
|
+
head.use(TemplateParamsPlugin);
|
|
15
|
+
const { canonicalQueryWhitelist, canonicalLowercase } = useRuntimeConfig().public["seo-utils"];
|
|
16
|
+
const route = useRoute();
|
|
17
|
+
const resolveUrl = createSitePathResolver({ withBase: true, absolute: true });
|
|
18
|
+
const err = useError();
|
|
19
|
+
const canonicalUrl = computed(() => {
|
|
20
|
+
if (err.value) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const { query } = route;
|
|
24
|
+
let url = resolveUrl(route.path || "/").value || route.path;
|
|
25
|
+
if (canonicalLowercase) {
|
|
26
|
+
try {
|
|
27
|
+
url = url.toLocaleLowerCase(resolveCurrentLocale());
|
|
28
|
+
} catch {
|
|
29
|
+
url = url.toLowerCase();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const filteredQuery = Object.fromEntries(
|
|
33
|
+
Object.entries(query).filter(([key]) => canonicalQueryWhitelist.includes(key)).sort(([a], [b]) => a.localeCompare(b))
|
|
34
|
+
// Sort params
|
|
35
|
+
);
|
|
36
|
+
const href = Object.keys(filteredQuery).length ? `${url}?${stringifyQuery(filteredQuery)}` : url;
|
|
37
|
+
return { rel: "canonical", href };
|
|
38
|
+
});
|
|
39
|
+
const minimalPriority = {
|
|
40
|
+
// give nuxt.config values higher priority
|
|
41
|
+
tagPriority: "low"
|
|
42
|
+
};
|
|
43
|
+
useHead({
|
|
44
|
+
htmlAttrs: { lang: resolveCurrentLocale },
|
|
45
|
+
templateParams: {
|
|
46
|
+
site: () => siteConfig,
|
|
47
|
+
siteName: () => siteConfig.name
|
|
48
|
+
},
|
|
49
|
+
titleTemplate: "%s %separator %siteName",
|
|
50
|
+
link: [() => canonicalUrl.value]
|
|
51
|
+
}, minimalPriority);
|
|
52
|
+
const seoMeta = {
|
|
53
|
+
ogType: "website",
|
|
54
|
+
ogUrl: () => {
|
|
55
|
+
const url = canonicalUrl.value;
|
|
56
|
+
return url ? url.href : false;
|
|
57
|
+
},
|
|
58
|
+
ogLocale: () => {
|
|
59
|
+
const locale = resolveCurrentLocale();
|
|
60
|
+
if (locale) {
|
|
61
|
+
const l = locale.replace("-", "_");
|
|
62
|
+
if (l.includes("_")) {
|
|
63
|
+
return l;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
},
|
|
68
|
+
ogSiteName: siteConfig.name
|
|
69
|
+
};
|
|
70
|
+
if (siteConfig.description)
|
|
71
|
+
seoMeta.description = siteConfig.description;
|
|
72
|
+
if (siteConfig.twitter) {
|
|
73
|
+
const id = siteConfig.twitter.startsWith("@") ? siteConfig.twitter : `@${siteConfig.twitter}`;
|
|
74
|
+
seoMeta.twitterCreator = id;
|
|
75
|
+
seoMeta.twitterSite = id;
|
|
76
|
+
}
|
|
77
|
+
useSeoMeta(seoMeta, minimalPriority);
|
|
78
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { injectHead, useSeoMeta } from "#imports";
|
|
2
|
+
import { defineNuxtPlugin, getRouteRules, useRequestEvent, useState } from "nuxt/app";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
enforce: "post",
|
|
5
|
+
async setup() {
|
|
6
|
+
const head = injectHead();
|
|
7
|
+
const routeRuleState = useState("nuxt-seo-utils:routeRules", () => null);
|
|
8
|
+
if (import.meta.server) {
|
|
9
|
+
const event = useRequestEvent();
|
|
10
|
+
const routeRules = await getRouteRules(event);
|
|
11
|
+
routeRuleState.value = {
|
|
12
|
+
head: routeRules.head,
|
|
13
|
+
seoMeta: routeRules.seoMeta
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
if (routeRuleState.value) {
|
|
17
|
+
const { head: headInput, seoMeta } = routeRuleState.value;
|
|
18
|
+
if (headInput)
|
|
19
|
+
head.push(headInput);
|
|
20
|
+
if (seoMeta)
|
|
21
|
+
useSeoMeta(seoMeta);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { injectHead } from "#imports";
|
|
2
|
+
import { createSitePathResolver } from "#site-config/app/composables/utils";
|
|
3
|
+
import { defineNuxtPlugin } from "nuxt/app";
|
|
4
|
+
import { unref } from "vue";
|
|
5
|
+
export default defineNuxtPlugin({
|
|
6
|
+
enforce: "post",
|
|
7
|
+
setup() {
|
|
8
|
+
const head = injectHead();
|
|
9
|
+
if (!head)
|
|
10
|
+
return;
|
|
11
|
+
const resolver = createSitePathResolver({
|
|
12
|
+
withBase: true,
|
|
13
|
+
absolute: true,
|
|
14
|
+
canonical: true
|
|
15
|
+
});
|
|
16
|
+
head.use({
|
|
17
|
+
key: "absoluteImageUrls",
|
|
18
|
+
hooks: {
|
|
19
|
+
"tags:resolve": async ({ tags }) => {
|
|
20
|
+
for (const tag of tags) {
|
|
21
|
+
if (tag.tag !== "meta")
|
|
22
|
+
continue;
|
|
23
|
+
if (tag.props.property !== "og:image:url" && tag.props.property !== "og:image" && tag.props.name !== "twitter:image")
|
|
24
|
+
continue;
|
|
25
|
+
if (typeof tag.props.content !== "string" || !tag.props.content.trim() || tag.props.content.startsWith("http") || tag.props.content.startsWith("//"))
|
|
26
|
+
continue;
|
|
27
|
+
tag.props.content = unref(resolver(tag.props.content));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from "nuxt/app";
|
|
2
|
+
import { applyDefaults } from "../logic/applyDefaults.js";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
name: "nuxt-seo:defaults",
|
|
5
|
+
order: 999,
|
|
6
|
+
env: {
|
|
7
|
+
islands: false
|
|
8
|
+
},
|
|
9
|
+
setup() {
|
|
10
|
+
applyDefaults();
|
|
11
|
+
}
|
|
12
|
+
});
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineNuxtPlugin } from "nuxt/app";
|
|
2
|
+
import { applyDefaults } from "../logic/applyDefaults.js";
|
|
3
|
+
export default defineNuxtPlugin({
|
|
4
|
+
name: "nuxt-seo:defaults",
|
|
5
|
+
env: {
|
|
6
|
+
islands: false
|
|
7
|
+
},
|
|
8
|
+
// we need to wait for the i18n plugin to run first
|
|
9
|
+
dependsOn: ["nuxt-site-config:i18n"],
|
|
10
|
+
setup() {
|
|
11
|
+
applyDefaults();
|
|
12
|
+
}
|
|
13
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { injectHead } from "#imports";
|
|
2
|
+
import { InferSeoMetaPlugin } from "@unhead/addons";
|
|
3
|
+
import { defineNuxtPlugin } from "nuxt/app";
|
|
4
|
+
import { TemplateParamsPlugin } from "unhead/plugins";
|
|
5
|
+
export default defineNuxtPlugin(() => {
|
|
6
|
+
const head = injectHead();
|
|
7
|
+
if (!head)
|
|
8
|
+
return;
|
|
9
|
+
head.use(TemplateParamsPlugin);
|
|
10
|
+
head.use(InferSeoMetaPlugin());
|
|
11
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { injectHead } from "#imports";
|
|
2
|
+
import { useSiteConfig } from "#site-config/app/composables/useSiteConfig";
|
|
3
|
+
import { defineNuxtPlugin } from "nuxt/app";
|
|
4
|
+
export default defineNuxtPlugin(() => {
|
|
5
|
+
const head = injectHead();
|
|
6
|
+
if (!head)
|
|
7
|
+
return;
|
|
8
|
+
const siteConfig = useSiteConfig();
|
|
9
|
+
const input = {
|
|
10
|
+
meta: [],
|
|
11
|
+
templateParams: {
|
|
12
|
+
site: siteConfig,
|
|
13
|
+
// support legacy
|
|
14
|
+
siteUrl: siteConfig.url,
|
|
15
|
+
siteName: siteConfig.name
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
if (siteConfig.separator)
|
|
19
|
+
input.templateParams.separator = siteConfig.separator;
|
|
20
|
+
if (siteConfig.titleSeparator)
|
|
21
|
+
input.templateParams.titleSeparator = siteConfig.titleSeparator;
|
|
22
|
+
if (siteConfig.description) {
|
|
23
|
+
input.templateParams.siteDescription = siteConfig.description;
|
|
24
|
+
input.meta.push(
|
|
25
|
+
{
|
|
26
|
+
name: "description",
|
|
27
|
+
content: "%site.description",
|
|
28
|
+
tagPriority: "low"
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
head.push(input);
|
|
33
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { useHead } from "#imports";
|
|
2
|
+
import { defineNuxtPlugin, useError, useRoute } from "nuxt/app";
|
|
3
|
+
import { titleCase } from "scule";
|
|
4
|
+
import { withoutTrailingSlash } from "ufo";
|
|
5
|
+
import { computed } from "vue";
|
|
6
|
+
export default defineNuxtPlugin({
|
|
7
|
+
name: "nuxt-seo:fallback-titles",
|
|
8
|
+
env: {
|
|
9
|
+
islands: false
|
|
10
|
+
},
|
|
11
|
+
setup() {
|
|
12
|
+
const route = useRoute();
|
|
13
|
+
const err = useError();
|
|
14
|
+
const title = computed(() => {
|
|
15
|
+
if (err.value && [404, 500].includes(err.value?.statusCode)) {
|
|
16
|
+
return `${err.value.statusCode} - ${err.value.message}`;
|
|
17
|
+
}
|
|
18
|
+
if (typeof route.meta?.title === "string")
|
|
19
|
+
return route.meta?.title;
|
|
20
|
+
const path = withoutTrailingSlash(route.path || "/");
|
|
21
|
+
const lastSegment = path.split("/").pop();
|
|
22
|
+
return lastSegment ? titleCase(lastSegment) : null;
|
|
23
|
+
});
|
|
24
|
+
const minimalPriority = {
|
|
25
|
+
// give nuxt.config values higher priority
|
|
26
|
+
tagPriority: 101
|
|
27
|
+
};
|
|
28
|
+
useHead({ title: () => title.value }, minimalPriority);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useSiteConfig } from "#site-config/server/composables/useSiteConfig";
|
|
2
|
+
import { defineEventHandler, getRequestHost, sendRedirect, setHeader } from "h3";
|
|
3
|
+
import { joinURL } from "ufo";
|
|
4
|
+
export default defineEventHandler((e) => {
|
|
5
|
+
if (import.meta.prerender) {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
const path = e.path;
|
|
9
|
+
if (path.startsWith("/_nuxt") || path.startsWith("/api")) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
if (path.split("/").pop()?.includes(".")) {
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const siteConfig = useSiteConfig(e);
|
|
16
|
+
if (!siteConfig.url || siteConfig.env !== "production") {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
const siteConfigHostName = new URL(e.path, siteConfig.url).host;
|
|
20
|
+
const host = getRequestHost(e, { xForwardedHost: true });
|
|
21
|
+
if (e.headers.get("x-nuxt-seo-redirected") || host.includes("http") || host.replace("https://", "").includes(":")) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
if (host.split(":")[0] !== siteConfigHostName.split(":")[0]) {
|
|
25
|
+
setHeader(e, "x-nuxt-seo-redirected", "true");
|
|
26
|
+
return sendRedirect(e, joinURL(siteConfig.url, e.path), 301);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { fileMapping } from "#seo-utils-virtual/pageDirImages";
|
|
3
|
+
import { defineEventHandler, setHeader } from "h3";
|
|
4
|
+
import { parseURL } from "ufo";
|
|
5
|
+
export default defineEventHandler(async (e) => {
|
|
6
|
+
const path = parseURL(e.path).pathname;
|
|
7
|
+
if (fileMapping[path]) {
|
|
8
|
+
if (path.endsWith(".svg"))
|
|
9
|
+
setHeader(e, "Content-Type", "image/svg+xml");
|
|
10
|
+
else if (path.endsWith(".png"))
|
|
11
|
+
setHeader(e, "Content-Type", "image/png");
|
|
12
|
+
else if (path.endsWith(".jpg") || path.endsWith(".jpeg"))
|
|
13
|
+
setHeader(e, "Content-Type", "image/jpeg");
|
|
14
|
+
return fs.readFileSync(fileMapping[path]);
|
|
15
|
+
}
|
|
16
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function pathBreadcrumbSegments(path: string, rootNode?: string): string[];
|