SEO
SEOにおいて最も重要なことは、ウェブ全体から広くリンクされる高品質なコンテンツを作成することです。しかし、サイトのランキングを上げるための技術的な考慮事項もいくつかあります。
設定不要
SSR
近年、検索エンジンはクライアントサイドのJavaScriptでレンダリングされたコンテンツのインデックス作成能力が向上しましたが、サーバーサイドでレンダリングされたコンテンツの方がより頻繁かつ確実にインデックスされます。SvelteKitはデフォルトでSSRを採用しており、handleで無効にすることはできますが、正当な理由がない限り有効にしておくべきです。
SvelteKitのレンダリングは高度に設定可能で、必要に応じて動的レンダリングを実装できます。一般的に、SSRにはSEO以外のメリットもあるため、あまり推奨されません。
パフォーマンス
Core Web Vitalsなどのシグナルは、検索エンジンのランキングに影響を与えます。SvelteとSvelteKitはオーバーヘッドを最小限に抑えるため、高性能なサイトを構築しやすくなっています。GoogleのPageSpeed InsightsやLighthouseを使用して、サイトのパフォーマンスをテストできます。詳細については、パフォーマンスのページを参照してください。
正規化されたURL
SvelteKitは、末尾にスラッシュが付いたパス名を(設定に応じて)付いていないものにリダイレクトします。これは、重複したURLがSEOにとって良くないためです。
手動設定
<title>と<meta>
すべてのページには、<svelte:head>内に適切に記述された、固有の<title>と<meta name="description">要素が必要です。説明的なタイトルと説明の書き方、および検索エンジンがコンテンツを理解できるようにするためのその他の提案については、GoogleのLighthouse SEO監査のドキュメントを参照してください。
一般的なパターンは、ページの
load関数からSEO関連のdataを返し、それをルートレイアウトの<svelte:head>で($page.dataとして)使用することです。
サイトマップ
サイトマップは、特に大量のコンテンツがある場合に、検索エンジンがサイト内のページを優先順位付けするのに役立ちます。エンドポイントを使用して、サイトマップを動的に作成できます。
export async function function GET(): Promise<Response>GET() {
return new var Response: new (body?: BodyInit | null, init?: ResponseInit) => ResponseThis Fetch API interface represents the response to a request.
Response(
`
<?xml version="1.0" encoding="UTF-8" ?>
<urlset
xmlns="https://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="https://www.w3.org/1999/xhtml"
xmlns:mobile="https://www.google.com/schemas/sitemap-mobile/1.0"
xmlns:news="https://www.google.com/schemas/sitemap-news/0.9"
xmlns:image="https://www.google.com/schemas/sitemap-image/1.1"
xmlns:video="https://www.google.com/schemas/sitemap-video/1.1"
>
<!-- <url> elements go here -->
</urlset>`.String.trim(): stringRemoves the leading and trailing white space and line terminator characters from a string.
trim(),
{
ResponseInit.headers?: HeadersInit | undefinedheaders: {
'Content-Type': 'application/xml'
}
}
);
}AMP
現代のウェブ開発における残念な現実は、サイトのAccelerated Mobile Pages (AMP)バージョンを作成する必要がある場合があることです。SvelteKitでは、inlineStyleThresholdオプションを設定することでこれを行うことができます...
/** @type {import('@sveltejs/kit').Config} */
const const config: {
kit: {
inlineStyleThreshold: number;
};
}
config = {
kit: {
inlineStyleThreshold: number;
}
kit: {
// since <link rel="stylesheet"> isn't
// allowed, inline all styles
inlineStyleThreshold: numberinlineStyleThreshold: var Infinity: numberInfinity
}
};
export default const config: {
kit: {
inlineStyleThreshold: number;
};
}
config;...ルートの+layout.js / +layout.server.jsでcsrを無効にし...
export const const csr: falsecsr = false;...ampをapp.htmlに追加し...
<html amp>
......@sveltejs/ampからインポートしたtransformとともにtransformPageChunkを使用してHTMLを変換します。
import * as import ampamp from '@sveltejs/amp';
/** @type {import('@sveltejs/kit').Handle} */
export async function function handle({ event, resolve }: {
event: any;
resolve: any;
}): Promise<any>
handle({ event: anyevent, resolve: anyresolve }) {
let let buffer: stringbuffer = '';
return await resolve: anyresolve(event: anyevent, {
transformPageChunk: ({ html, done }: {
html: any;
done: any;
}) => string | undefined
transformPageChunk: ({ html: anyhtml, done: anydone }) => {
let buffer: stringbuffer += html: anyhtml;
if (done: anydone) return import ampamp.function transform(html: string): stringtransform(let buffer: stringbuffer);
}
});
}import * as import ampamp from '@sveltejs/amp';
import type { type Handle = (input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}) => MaybePromise<...>
The handle hook runs every time the SvelteKit server receives a request and
determines the response.
It receives an event object representing the request and a function called resolve, which renders the route and generates a Response.
This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
Handle } from '@sveltejs/kit';
export const const handle: Handlehandle: type Handle = (input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}) => MaybePromise<...>
The handle hook runs every time the SvelteKit server receives a request and
determines the response.
It receives an event object representing the request and a function called resolve, which renders the route and generates a Response.
This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
Handle = async ({ event: RequestEvent<Partial<Record<string, string>>, string | null>event, resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve }) => {
let let buffer: stringbuffer = '';
return await resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve(event: RequestEvent<Partial<Record<string, string>>, string | null>event, {
ResolveOptions.transformPageChunk?(input: {
html: string;
done: boolean;
}): MaybePromise<string | undefined>
Applies custom transforms to HTML. If done is true, it’s the final chunk. Chunks are not guaranteed to be well-formed HTML
(they could include an element’s opening tag but not its closing tag, for example)
but they will always be split at sensible boundaries such as %sveltekit.head% or layout/page components.
transformPageChunk: ({ html: stringhtml, done: booleandone }) => {
let buffer: stringbuffer += html: stringhtml;
if (done: booleandone) return import ampamp.function transform(html: string): stringtransform(let buffer: stringbuffer);
}
});
};ページをAMPに変換した結果として未使用のCSSが出荷されるのを防ぐために、dropcssを使用できます。
import * as import ampamp from '@sveltejs/amp';
import module "dropcss"dropcss from 'dropcss';
/** @type {import('@sveltejs/kit').Handle} */
export async function function handle(input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}): MaybePromise<...>
handle({ event: RequestEvent<Partial<Record<string, string>>, string | null>event, resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve }) {
let let buffer: stringbuffer = '';
return await resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve(event: RequestEvent<Partial<Record<string, string>>, string | null>event, {
ResolveOptions.transformPageChunk?(input: {
html: string;
done: boolean;
}): MaybePromise<string | undefined>
Applies custom transforms to HTML. If done is true, it’s the final chunk. Chunks are not guaranteed to be well-formed HTML
(they could include an element’s opening tag but not its closing tag, for example)
but they will always be split at sensible boundaries such as %sveltekit.head% or layout/page components.
transformPageChunk: ({ html: stringhtml, done: booleandone }) => {
let buffer: stringbuffer += html: stringhtml;
if (done: booleandone) {
let let css: stringcss = '';
const const markup: stringmarkup = import ampamp
.function transform(html: string): stringtransform(let buffer: stringbuffer)
.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)Replaces text in a string, using a regular expression or search string.
replace('⚡', 'amp') // dropcss can't handle this character
.String.replace(searchValue: {
[Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string;
}, replacer: (substring: string, ...args: any[]) => string): string (+3 overloads)
Replaces text in a string, using an object that supports replacement within a string.
replace(/<style amp-custom([^>]*?)>([^]+?)<\/style>/, (match: stringmatch, attributes: anyattributes, contents: anycontents) => {
let css: stringcss = contents: anycontents;
return `<style amp-custom${attributes: anyattributes}></style>`;
});
let css: stringcss = module "dropcss"dropcss({ css: stringcss, html: stringhtml: const markup: stringmarkup }).css;
return const markup: stringmarkup.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)Replaces text in a string, using a regular expression or search string.
replace('</style>', `${let css: stringcss}</style>`);
}
}
});
}
import * as import ampamp from '@sveltejs/amp';
import module "dropcss"dropcss from 'dropcss';
import type { type Handle = (input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}) => MaybePromise<...>
The handle hook runs every time the SvelteKit server receives a request and
determines the response.
It receives an event object representing the request and a function called resolve, which renders the route and generates a Response.
This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
Handle } from '@sveltejs/kit';
export const const handle: Handlehandle: type Handle = (input: {
event: RequestEvent;
resolve(event: RequestEvent, opts?: ResolveOptions): MaybePromise<Response>;
}) => MaybePromise<...>
The handle hook runs every time the SvelteKit server receives a request and
determines the response.
It receives an event object representing the request and a function called resolve, which renders the route and generates a Response.
This allows you to modify response headers or bodies, or bypass SvelteKit entirely (for implementing routes programmatically, for example).
Handle = async ({ event: RequestEvent<Partial<Record<string, string>>, string | null>event, resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve }) => {
let let buffer: stringbuffer = '';
return await resolve: (event: RequestEvent, opts?: ResolveOptions) => MaybePromise<Response>resolve(event: RequestEvent<Partial<Record<string, string>>, string | null>event, {
ResolveOptions.transformPageChunk?(input: {
html: string;
done: boolean;
}): MaybePromise<string | undefined>
Applies custom transforms to HTML. If done is true, it’s the final chunk. Chunks are not guaranteed to be well-formed HTML
(they could include an element’s opening tag but not its closing tag, for example)
but they will always be split at sensible boundaries such as %sveltekit.head% or layout/page components.
transformPageChunk: ({ html: stringhtml, done: booleandone }) => {
let buffer: stringbuffer += html: stringhtml;
if (done: booleandone) {
let let css: stringcss = '';
const const markup: stringmarkup = import ampamp
.function transform(html: string): stringtransform(let buffer: stringbuffer)
.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)Replaces text in a string, using a regular expression or search string.
replace('⚡', 'amp') // dropcss can't handle this character
.String.replace(searchValue: {
[Symbol.replace](string: string, replacer: (substring: string, ...args: any[]) => string): string;
}, replacer: (substring: string, ...args: any[]) => string): string (+3 overloads)
Replaces text in a string, using an object that supports replacement within a string.
replace(/<style amp-custom([^>]*?)>([^]+?)<\/style>/, (match: stringmatch, attributes: anyattributes, contents: anycontents) => {
let css: stringcss = contents: anycontents;
return `<style amp-custom${attributes: anyattributes}></style>`;
});
let css: stringcss = module "dropcss"dropcss({ css: stringcss, html: stringhtml: const markup: stringmarkup }).css;
return const markup: stringmarkup.String.replace(searchValue: string | RegExp, replaceValue: string): string (+3 overloads)Replaces text in a string, using a regular expression or search string.
replace('</style>', `${let css: stringcss}</style>`);
}
}
});
};
変換されたHTMLを
amphtml-validatorを使用して検証するためにhandleフックを使用することをお勧めしますが、非常に遅いため、ページのプリレンダリングを行っている場合に限ります。