feat: introduce define helpers (#1737)

This commit is contained in:
Antoine Lethimonnier 2025-10-23 02:51:53 +02:00 committed by GitHub
parent 987e1ba427
commit 3ccac3e0de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 263 additions and 87 deletions

View File

@ -0,0 +1,5 @@
---
'svelte-meta-tags': minor
---
feat: add helper functions to define meta tags objects

View File

@ -29,15 +29,15 @@ Use this when you want to override the default values on child pages, as in the
## +layout.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { defineBaseMetaTags } from 'svelte-meta-tags';
export const load = ({ url }) => {
const baseMetaTags = Object.freeze({
const baseTags = defineBaseMetaTags({
title: 'Default',
titleTemplate: '%s | Svelte Meta Tags',
description: 'Svelte Meta Tags is a Svelte component for managing meta tags and SEO in your Svelte applications.',
canonical: new URL(url.pathname, url.origin).href,
canonical: new URL(url.pathname, url.origin).href, // creates a cleaned up URL (without hashes or query params) from your current URL
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
@ -56,31 +56,27 @@ export const load = ({ url }) => {
}
]
}
}) satisfies MetaTagsProps;
});
return {
baseMetaTags
};
return { ...baseTags };
};
```
## +page.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { definePageMetaTags } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
const pageTags = definePageMetaTags({
title: 'TOP',
description: 'Description TOP',
openGraph: {
title: 'Open Graph Title TOP',
description: 'Open Graph Description TOP'
}
}) satisfies MetaTagsProps;
});
return {
pageMetaTags
};
return { ...pageTags };
};
```

View File

@ -29,15 +29,15 @@ sidebar:
## +layout.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { defineBaseMetaTags } from 'svelte-meta-tags';
export const load = ({ url }) => {
const baseMetaTags = Object.freeze({
const baseTags = defineBaseMetaTags({
title: 'Default',
titleTemplate: '%s | Svelte Meta Tags',
description: 'Svelte Meta Tags is a Svelte component for managing meta tags and SEO in your Svelte applications.',
canonical: new URL(url.pathname, url.origin).href,
canonical: new URL(url.pathname, url.origin).href, // 現在のURLからクリーンなURLハッシュやクエリパラメータなしを作成します
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
@ -56,31 +56,27 @@ export const load = ({ url }) => {
}
]
}
}) satisfies MetaTagsProps;
});
return {
baseMetaTags
};
return { ...baseTags };
};
```
## +page.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { definePageMetaTags } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
const pageTags = definePageMetaTags({
title: 'TOP',
description: 'Description TOP',
openGraph: {
title: 'Open Graph Title TOP',
description: 'Open Graph Description TOP'
}
}) satisfies MetaTagsProps;
});
return {
pageMetaTags
};
return { ...pageTags };
};
```

View File

@ -67,7 +67,7 @@ import { LinkButton } from '@astrojs/starlight/components';
/>
```
## 子ページでデフォルト値を上書きする
## 子ページでデフォルト値を上書きする:
[Example](https://github.com/oekazuma/svelte-meta-tags/tree/main/example)
@ -90,15 +90,15 @@ import { LinkButton } from '@astrojs/starlight/components';
### +layout.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { defineBaseMetaTags } from 'svelte-meta-tags';
export const load = ({ url }) => {
const baseMetaTags = Object.freeze({
const baseTags = defineBaseMetaTags({
title: 'Default',
titleTemplate: '%s | Svelte Meta Tags',
description: 'Svelte Meta Tags is a Svelte component for managing meta tags and SEO in your Svelte applications.',
canonical: new URL(url.pathname, url.origin).href,
canonical: new URL(url.pathname, url.origin).href, // 現在のURLからクリーンなURLハッシュやクエリパラメータなしを作成します
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
@ -117,31 +117,39 @@ export const load = ({ url }) => {
}
]
}
}) satisfies MetaTagsProps;
});
return {
baseMetaTags
};
return { ...baseTags };
};
```
> 注意: `defineBaseMetaTags`は、`+layout.(j|t)s`ファイルで使用することを意図したユーティリティです。
> このユーティリティは、フリーズされた`MetaTagsProps`オブジェクトを`baseMetaTags`プロパティにラップして返し、ロードデータで直接使用できます。
>
> このユーティリティを使用せずに、`return { baseMetaTags: Object.freeze<MetaTagsProps>({ ... }) }`のように生のオブジェクトを提供することもできます。
> `MetaTagsProps`は、`import type { MetaTagsProps } from 'svelte-meta-tags'`を使用してインポートできる、このパッケージで提供される型です。
### +page.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { definePageMetaTags } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
const pageTags = definePageMetaTags({
title: 'TOP',
description: 'Description TOP',
openGraph: {
title: 'Open Graph Title TOP',
description: 'Open Graph Description TOP'
}
}) satisfies MetaTagsProps;
});
return {
pageMetaTags
};
return { ...pageTags };
};
```
> 注意: `defineBaseMetaTags`と同様に、`definePageMetaTags`は`+page.(j|t)s`ファイルで使用することを意図したユーティリティです。
> このユーティリティは、フリーズされた`MetaTagsProps`オブジェクトを`pageMetaTags`プロパティにラップして返し、ロードデータで直接使用できます。
>
> このユーティリティを使用せずに、`return { pageMetaTags: Object.freeze<MetaTagsProps>({ ... }) }`のように生のオブジェクトを提供することもできます。
> `MetaTagsProps`は、`import type { MetaTagsProps } from 'svelte-meta-tags'`を使用してインポートできる、このパッケージで提供される型です。

View File

@ -90,15 +90,15 @@ import { LinkButton } from '@astrojs/starlight/components';
### +layout.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { defineBaseMetaTags } from 'svelte-meta-tags';
export const load = ({ url }) => {
const baseMetaTags = Object.freeze({
const baseTags = defineBaseMetaTags({
title: 'Default',
titleTemplate: '%s | Svelte Meta Tags',
description: 'Svelte Meta Tags is a Svelte component for managing meta tags and SEO in your Svelte applications.',
canonical: new URL(url.pathname, url.origin).href,
canonical: new URL(url.pathname, url.origin).href, // creates a cleaned up URL (without hashes or query params) from your current URL
openGraph: {
type: 'website',
url: new URL(url.pathname, url.origin).href,
@ -117,31 +117,39 @@ export const load = ({ url }) => {
}
]
}
}) satisfies MetaTagsProps;
});
return {
baseMetaTags
};
return { ...baseTags };
};
```
> Note: `defineBaseMetaTags` is a utility meant to be used in a `+layout.(j|t)s` file.
> It returns a frozen `MetaTagsProps` object wrapped in a `baseMetaTags` property for direct use in your load data.
>
> You can also provide a raw object without this utility with `return { baseMetaTags: Object.freeze<MetaTagsProps>({ ... }) }`.
> `MetaTagsProps` is a type provided by this package, imported using `import type { MetaTagsProps } from 'svelte-meta-tags'`.
### +page.ts
```svelte
import type { MetaTagsProps } from 'svelte-meta-tags';
```ts
import { definePageMetaTags } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
const pageTags = definePageMetaTags({
title: 'TOP',
description: 'Description TOP',
openGraph: {
title: 'Open Graph Title TOP',
description: 'Open Graph Description TOP'
}
}) satisfies MetaTagsProps;
});
return {
pageMetaTags
};
return { ...pageTags };
};
```
> Note: like `defineBaseMetaTags`, `definePageMetaTags` is a utility meant to be used in a `+page.(j|t)s` file.
> It returns a frozen `MetaTagsProps` object wrapped in a `pageMetaTags` property for direct use in your load data.
>
> You can also provide a raw object without this utility with `return { pageMetaTags: Object.freeze<MetaTagsProps>({ ... }) }`.
> `MetaTagsProps` is a type provided by this package, imported using `import type { MetaTagsProps } from 'svelte-meta-tags'`.

View File

@ -1,16 +1,9 @@
<script lang="ts">
import type { LayoutData } from './$types';
import { page } from '$app/state';
import type { Snippet } from 'svelte';
import { MetaTags, deepMerge } from 'svelte-meta-tags';
import { resolve } from '$app/paths';
interface Props {
data: LayoutData;
children: Snippet;
}
let { data, children }: Props = $props();
let { data, children } = $props();
let metaTags = $derived(deepMerge(data.baseMetaTags, page.data.pageMetaTags));
</script>

View File

@ -1,7 +1,7 @@
import type { MetaTagsProps } from 'svelte-meta-tags';
import { defineBaseMetaTags } from 'svelte-meta-tags';
export const load = ({ url }) => {
const baseMetaTags = Object.freeze({
const baseTags = defineBaseMetaTags({
title: 'Normal',
titleTemplate: '%s | Svelte Meta Tags',
description: 'Svelte Meta Tags is a Svelte component for managing meta tags and SEO in your Svelte applications.',
@ -40,9 +40,7 @@ export const load = ({ url }) => {
}
]
}
}) satisfies MetaTagsProps;
});
return {
baseMetaTags
};
return { ...baseTags };
};

View File

@ -1,16 +1,14 @@
import type { MetaTagsProps } from 'svelte-meta-tags';
import { definePageMetaTags } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
const pageTags = definePageMetaTags({
title: 'TOP',
description: 'Description TOP',
openGraph: {
title: 'Open Graph Title TOP',
description: 'Open Graph Description TOP'
}
}) satisfies MetaTagsProps;
});
return {
pageMetaTags
};
return { ...pageTags };
};

View File

@ -1,16 +1,14 @@
import type { MetaTagsProps } from 'svelte-meta-tags';
import { definePageMetaTags } from 'svelte-meta-tags';
export const load = () => {
const pageMetaTags = Object.freeze({
const pageTags = definePageMetaTags({
title: 'About',
description: 'Description About',
openGraph: {
title: 'Open Graph Title About',
description: 'Open Graph Description About'
}
}) satisfies MetaTagsProps;
});
return {
pageMetaTags
};
return { ...pageTags };
};

View File

@ -0,0 +1,46 @@
import type { MetaTagsProps } from './types';
/**
* A convenience wrapper for creating a readonly, type-safe
* {@link MetaTagsProps} object for `+layout`.
*
* @param obj the input props
* @returns a frozen copy of the input props inside a ready-to-use object
*
* @example
* ```typescript
* // In +layout.ts
* export const load = () => {
* const baseTags = defineBaseMetaTags({
* title: 'My App',
* description: 'Welcome to my application'
* });
* return { ...baseTags };
* };
* ```
*/
export const defineBaseMetaTags = (obj: MetaTagsProps) => ({ baseMetaTags: Object.freeze(obj) });
/**
* A convenience wrapper for creating a readonly, type-safe
* {@link MetaTagsProps} object for `+page`.
*
* @param obj the input props
* @returns a frozen copy of the input props inside a ready-to-use object
*
* @example
* ```typescript
* // In +page.ts
* export const load = () => {
* const pageTags = definePageMetaTags({
* title: 'About Us',
* description: 'Learn more about our company'
* });
* return { ...pageTags };
* };
*
* // In +layout.svelte
* const metaTags = deepMerge(data.baseMetaTags, page.data.pageMetaTags);
* ```
*/
export const definePageMetaTags = (obj: MetaTagsProps) => ({ pageMetaTags: Object.freeze(obj) });

View File

@ -1,6 +1,7 @@
export { default as MetaTags } from './MetaTags.svelte';
export { default as JsonLd } from './JsonLd.svelte';
export { deepMerge } from './deepMerge';
export { defineBaseMetaTags, definePageMetaTags } from './define';
export type {
MetaTagsProps,
JsonLdProps,

View File

@ -0,0 +1,129 @@
import { describe, expect, test } from 'vitest';
import { defineBaseMetaTags, definePageMetaTags } from '$lib/define';
import type { MetaTagsProps } from '$lib/types';
describe('defineBaseMetaTags and definePageMetaTags', () => {
const sampleMetaTags: MetaTagsProps = {
title: 'Test Title',
description: 'Test description',
canonical: 'https://example.com'
};
test('should create frozen readonly object', () => {
const { baseMetaTags } = defineBaseMetaTags(sampleMetaTags);
const { pageMetaTags } = definePageMetaTags(sampleMetaTags);
expect(baseMetaTags).toEqual(sampleMetaTags);
expect(pageMetaTags).toEqual(sampleMetaTags);
expect(Object.isFrozen(baseMetaTags)).toBe(true);
expect(Object.isFrozen(pageMetaTags)).toBe(true);
});
test('should freeze the props to make them readonly', () => {
const { baseMetaTags } = defineBaseMetaTags(sampleMetaTags);
expect(Object.isFrozen(baseMetaTags)).toBe(true);
// Should throw when trying to modify frozen object
expect(() => {
// @ts-expect-error - intentionally testing runtime mutation
baseMetaTags.title = 'Modified';
}).toThrow(TypeError);
});
test('should not modify the original input object', () => {
const original = { ...sampleMetaTags };
const { baseMetaTags } = defineBaseMetaTags(sampleMetaTags);
expect(sampleMetaTags).toEqual(original);
expect(baseMetaTags).toEqual(original);
});
test('should handle empty object', () => {
const { baseMetaTags } = defineBaseMetaTags({});
expect(baseMetaTags).toEqual({});
expect(Object.isFrozen(baseMetaTags)).toBe(true);
});
test('should handle falsy values correctly', () => {
const metaTagsWithFalsy: MetaTagsProps = {
title: '',
robots: false,
keywords: []
};
const { pageMetaTags } = definePageMetaTags(metaTagsWithFalsy);
expect(pageMetaTags.title).toBe('');
expect(pageMetaTags.robots).toBe(false);
expect(pageMetaTags.keywords).toEqual([]);
});
test('should support spreading for object merging', () => {
const { baseMetaTags } = defineBaseMetaTags({
title: 'Base Title',
description: 'Base Description'
});
const { pageMetaTags } = definePageMetaTags({
title: 'Page Title',
canonical: 'https://example.com/page'
});
const combined = { ...baseMetaTags, ...pageMetaTags };
expect(combined.title).toBe('Page Title'); // Page overrides base
expect(combined.description).toBe('Base Description'); // From base
expect(combined.canonical).toBe('https://example.com/page'); // From page
});
test('should handle complex nested objects', () => {
const complexMetaTags: MetaTagsProps = {
title: 'Complex Title',
openGraph: {
title: 'OG Title',
images: [{ url: 'https://example.com/image.jpg' }]
},
additionalMetaTags: [{ name: 'viewport', content: 'width=device-width' }]
};
const { baseMetaTags } = defineBaseMetaTags(complexMetaTags);
expect(baseMetaTags.openGraph?.images?.[0]?.url).toBe('https://example.com/image.jpg');
expect(Object.isFrozen(baseMetaTags)).toBe(true);
});
test('both functions should behave identically', () => {
const { baseMetaTags } = defineBaseMetaTags(sampleMetaTags);
const { pageMetaTags } = definePageMetaTags(sampleMetaTags);
expect(baseMetaTags).toEqual(pageMetaTags);
expect(Object.isFrozen(baseMetaTags)).toBe(Object.isFrozen(pageMetaTags));
});
test('should clarify that freeze is shallow (nested objects remain mutable)', () => {
const complexMetaTags: MetaTagsProps = {
title: 'Complex Title',
openGraph: {
title: 'OG Title',
images: [{ url: 'https://example.com/image.jpg' }]
}
};
const { baseMetaTags } = defineBaseMetaTags(complexMetaTags);
// Top level is frozen
expect(Object.isFrozen(baseMetaTags)).toBe(true);
// But nested objects are NOT frozen (limitation of Object.freeze)
expect(Object.isFrozen(baseMetaTags.openGraph)).toBe(false);
expect(Object.isFrozen(baseMetaTags.openGraph?.images)).toBe(false);
// Nested objects can still be mutated
if (baseMetaTags.openGraph) {
baseMetaTags.openGraph.title = 'Modified OG Title'; // This works!
expect(baseMetaTags.openGraph.title).toBe('Modified OG Title');
}
});
});