介绍

Laravel Head 提供了一套流畅的 API,用于管理应用程序的文档 <head> 元素,包括标题和元标签、Open Graph 元数据、规范 URL、robots 指令、性能提示以及结构化数据。它可与 Blade、Livewire 和 Inertia 配合使用。

安装

您可以使用 Composer 包管理器安装 Laravel Head:

composer require laravel/head

快速入门

在服务提供者中注册全站默认设置:

use Laravel\Head\Facades\Head;
use Laravel\Head\HeadBuilder;

Head::defaults(fn (HeadBuilder $head) => $head
    ->title('Laravel', suffix: ' - Laravel')
    ->description('Build something great.'));

在运行时设置页面特定的元数据:

Head::title($post->title)
    ->description($post->description);

在布局中渲染解析后的标签:

<head>
    @head
</head>

解析优先级

页面元数据从五个层级解析,优先级从低到高排列如下:

  1. 页面默认设置
  2. 路由组元数据
  3. 路由元数据
  4. 运行时元数据
  5. 错误元数据

较高层级会逐字段替换较低层级的内容。例如,运行时的标题会替换路由的标题,但不会替换路由的描述。后续章节将说明如何为每个层级设置元数据。有关在 Blade、Livewire 和 Inertia 中渲染解析后元数据的信息,请参阅渲染。

定义元数据

Laravel Head 允许您通过全站默认设置、路由元数据、运行时调用以及错误页面定义来定义元数据。

默认设置

在服务提供者中注册页面默认设置:

use Laravel\Head\Enums\OgType;
use Laravel\Head\Facades\Head;
use Laravel\Head\HeadBuilder;

Head::defaults(function (HeadBuilder $head) {
    $head
        ->title('Laravel', suffix: ' - Laravel')
        ->description('Build something great.')
        ->canonical()
        ->og(siteName: 'Laravel', type: OgType::Website)
        ->searchableByRobots()
        ->preconnect('https://fonts.example.com');
});

默认设置是优先级最低的页面元数据层级。如果没有路由、运行时或错误元数据设置标题,Laravel 将原样渲染。当较高层级设置了页面标题时,会应用继承的后缀,因此 Head::title('About') 会渲染为 About - Laravel。对于应忽略继承的前缀或后缀的标题,请传入 exact: true。

调用 Head::canonical() 会使用当前请求 URL 渲染规范 URL。若要设置显式 URL,请传入字符串,例如 Head::canonical('/about')。规范 URL 默认会规范化为 https;传入 forceHttps: false 可保留请求的协议。

robots 指令可以传入原始字符串、RobotsRule 枚举值,或是混合两种形式的列表。列表会渲染为以逗号分隔的指令,因此 Head::robots([RobotsRule::NoIndex, RobotsRule::NoFollow]) 会渲染为 noindex, nofollow。

为方便起见,searchableByRobots 方法会渲染 all,而 hiddenFromRobots 方法会渲染 none。

路由元数据

您可以直接在路由上定义元数据,这对于元数据可提前确定的半静态页面尤其有用。

路由与路由组

Route::view('/contact', 'contact')
    ->name('contact')
    ->withHead(
        title: 'Contact Us',
        description: 'Get in touch.',
    );

共享的路由元数据可以在链式调用的任意位置应用于路由组:

Route::withHead(robots: 'noindex, nofollow')
    ->prefix('admin')
    ->name('admin.')
    ->group(function () {
        Route::get('/dashboard', DashboardController::class)
            ->name('dashboard')
            ->withHead(title: 'Dashboard');
    });

您还可以为资源路由和单例路由定义元数据:

Route::resource('posts', PostController::class)->withHead(
    robots: 'index, follow',
);

Route::singleton('profile', ProfileController::class)->withHead(
    title: 'Your Profile',
);

withHead 方法通过 Laravel 原生的路由元数据 API 存储普通数组。它等效于调用 metadata 方法,并将属性嵌套在 head 键下,因此元数据保持与缓存路由兼容。

命名参数有意限制为 Laravel Head 内置的路由属性,以便编辑器和静态分析工具能够捕获拼写错误。自定义标签构建器注册的路由属性可通过 extensions 传入:

Route::get('/article', ArticleController::class)->withHead(
    title: 'Article',
    extensions: ['readingTime' => 4],
);

支持的属性

支持的路由属性与流畅构建器方法的名称一一对应:

类别属性
文档title、description、canonical、robots
应用元数据themeColor、applicationName、colorScheme、referrer、viewport、appleWebAppTitle、webAppCapable、appleWebAppStatusBarStyle
社交og、ogImage、ogVideo、ogAudio、twitter、twitterImage
性能preload、prefetch、preconnect、dnsPrefetch
发现alternates、feed、icon、favicon、appleTouchIcon、appleTouchStartupImage、maskIcon、manifest
结构化数据schema
自定义标签meta、link

嵌套选项名称使用与流畅 API 相同的 camelCase 命名方式,例如 forceHttps、siteName 和 secureUrl。

可重复的属性,例如 ogImage、preload、feed、schema、icon 和 appleTouchStartupImage,既可以接收单个值,也可以接收列表。

运行时元数据

当某个值在请求到达之前未知时,例如正在查看的文章的标题,您可以在运行时进行设置:

use Laravel\Head\Facades\Head;

public function __invoke(Post $post): Response
{
    Head::title($post->title);

    // ...
}

通过 Head 门面发起的运行时调用会覆盖路由元数据,用于处理依赖请求的数据。控制器和动作是发起这些调用的最常见位置:

use App\Models\Post;
use Laravel\Head\Facades\Head;

public function show(Post $post)
{
    Head::title($post->title)
        ->description($post->description);

    return view('posts.show', ['post' => $post]);
}

多次运行时调用会按其执行顺序进行合并。对于单值字段,例如标题、描述、规范 URL 和 robots 指令,后一次调用优先。可重复字段会保留多条记录,但再次添加相同的键会更新之前的条目。对于 ogImage 方法,URL 即为键:

Head::ogImage('/images/cover.jpg', alt: 'Draft cover')
    ->ogImage('/images/gallery.jpg', alt: 'Gallery image')
    ->ogImage('/images/cover.jpg', alt: 'Final cover', width: 1200, height: 630);
<meta property="og:image" content="/images/cover.jpg">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Final cover">
<meta property="og:image" content="/images/gallery.jpg">
<meta property="og:image:alt" content="Gallery image">

从默认设置继承而来的 Open Graph 媒体会作为后备内容。当路由、运行时或错误元数据定义了相同类型的自有媒体时,默认媒体会被替换而非合并,因此页面的 og:image 优先级高于全站默认图片。

您可以使用 when 和 unless 方法流畅地定义条件元数据:

Head::title($post->title)
    ->when($post->isDraft(), fn ($head) => $head->hiddenFromRobots());

错误页面

通常,您应在应用程序的 AppServiceProvider 类的 boot 方法中注册错误元数据:

use Laravel\Head\ErrorPages;
use Laravel\Head\Facades\Head;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    Head::errors(function (ErrorPages $errors) {
        $errors->defaults(robots: 'noindex, follow');

        $errors->status(
            404,
            title: 'Page Not Found',
            description: 'The page you are looking for could not be found.',
        );
    });
}

defaults 和 status 方法也接受 Head::defaults() 所使用的同一种流式构建器回调:

use Laravel\Head\ErrorPages;
use Laravel\Head\Facades\Head;
use Laravel\Head\HeadBuilder;

Head::errors(function (ErrorPages $errors) {
    $errors->status(404, fn (HeadBuilder $head) => $head
        ->title('Page Not Found')
        ->description('The page you are looking for could not be found.'));
});

当为已注册的错误状态渲染响应时,该元数据会优先于其他所有层级。

在渲染错误视图或执行响应阶段钩子(如 Inertia 的 handleExceptionsUsing() 方法)时,Laravel 会自动检测响应状态。如果在 $exceptions->render() 回调中渲染错误响应,请在渲染前调用 Head::status(404),以便应用错误元数据。

Open Graph

您可以使用 og 方法设置 Open Graph 属性。可重复的媒体资源可通过顶层方法添加,这些方法直接接受具名参数:

use Laravel\Head\Enums\ImageType;
use Laravel\Head\Enums\OgType;

Head::og(type: OgType::Article, title: $post->title)
    ->ogImage($post->hero_image_url)
    ->ogImage(
        $post->gallery_image_url,
        alt: $post->gallery_image_alt,
        width: 1200,
        height: 630,
        type: ImageType::Jpeg,
    );

ogImage、ogVideo 和 ogAudio 方法的第一个参数为 URL,同时还支持可选的具名参数,例如 alt、width、height、type 和 secureUrl(在 Open Graph 规范支持的情况下)。

在 API 接受图像 type 参数的任何地方,都可以传入 ImageType 枚举值,例如 ImageType::Svg、ImageType::Png、ImageType::Jpeg 和 ImageType::Webp。

文档的 title 和 description 会自动填充缺失的 og:title 和 og:description 值。

对于不包含其他属性的单个 Open Graph 图像,可以向 og 方法传入 image 具名参数:

Head::og(
    type: OgType::Website,
    title: $page->title,
    description: $page->description,
    image: $page->og_image_url,
);

og(image: ...) 和 ogImage(...) 调用会写入同一个底层图像列表,因此在调用处可以选择表达更清晰的方式。对于自定义的 Open Graph 扩展(如产品或文章属性),可以使用 meta 方法。

X / Twitter 卡片

要使用 Open Graph 所用的标题、描述和图像来渲染 X / Twitter 卡片,请在默认配置中注册 twitter():

use Laravel\Head\Enums\TwitterCard;
use Laravel\Head\Facades\Head;
use Laravel\Head\HeadBuilder;

Head::defaults(fn (HeadBuilder $head) => $head->twitter(
    card: TwitterCard::SummaryWithLargeImage,
));

然后设置页面级元数据:

Head::title('Introducing Laravel Head')
    ->description('A fluent API for Laravel document head metadata.')
    ->ogImage('https://example.com/social.jpg', alt: 'Introducing Laravel Head');

这会渲染出匹配的 Twitter 标签:

<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="Introducing Laravel Head">
<meta name="twitter:description" content="A fluent API for Laravel document head metadata.">
<meta name="twitter:image" content="https://example.com/social.jpg">
<meta name="twitter:image:alt" content="Introducing Laravel Head">

可以为单个页面设置显式的 Twitter 值进行自定义:

Head::twitter(title: $post->social_title)
    ->twitterImage($post->social_image_url, alt: $post->title);

路由元数据支持 twitter 和 twitterImage。

主题颜色

您可以全局、按路由或在运行时设置主题颜色:

Head::themeColor('#0f172a');

这会渲染出一个 <meta name="theme-color"> 标签。对于特定媒体的主题颜色,可以使用 Media 枚举:

use Laravel\Head\Enums\Media;

Head::themeColor('#ffffff', media: Media::Light)
    ->themeColor('#111827', media: Media::Dark);

Media 枚举还包含 Portrait 和 Landscape。media 参数也支持自定义媒体查询字符串。

路由元数据可通过相同的 camelCase 键支持单个主题颜色:

Route::view('/dashboard', 'dashboard')->withHead(
    themeColor: '#0f172a',
);

应用元数据和图标

Laravel Head 包含了用于常见浏览器和应用元数据的方法:

use Laravel\Head\Enums\ImageType;
use Laravel\Head\Enums\Media;

Head::applicationName('Laravel')
    ->colorScheme('light dark')
    ->referrer('strict-origin-when-cross-origin')
    ->viewport('width=device-width, initial-scale=1')
    ->appleWebAppTitle('Laravel')
    ->webAppCapable()
    ->appleWebAppStatusBarStyle('black')
    ->favicon('/favicon.svg', type: ImageType::Svg)
    ->icon('/favicon-32x32.png', type: ImageType::Png, sizes: '32x32')
    ->appleTouchIcon('/apple-touch-icon.png', sizes: '180x180')
    ->appleTouchStartupImage('/launch.png', media: Media::Portrait)
    ->maskIcon('/safari-pinned-tab.svg', color: '#111827')
    ->manifest('/site.webmanifest');

favicon 方法是 icon 方法的别名,接受相同的 type、sizes 和 media 参数。

路由元数据使用相同的名称:

use Laravel\Head\Enums\ImageType;
use Laravel\Head\Enums\Media;

Route::view('/dashboard', 'dashboard')->withHead(
    applicationName: 'Laravel',
    colorScheme: 'light dark',
    appleWebAppTitle: 'Laravel',
    webAppCapable: true,
    appleWebAppStatusBarStyle: 'black',
    favicon: [
        ['href' => '/favicon.svg', 'type' => ImageType::Svg],
        ['href' => '/favicon-32x32.png', 'type' => ImageType::Png, 'sizes' => '32x32'],
    ],
    appleTouchIcon: ['href' => '/apple-touch-icon.png', 'sizes' => '180x180'],
    appleTouchStartupImage: ['href' => '/launch.png', 'media' => Media::Portrait],
    manifest: '/site.webmanifest',
);

渐进式 Web 应用

pwa 方法用于配置可安装 Web 应用所需的常见文档 <head> 标签:

Head::pwa(
    name: 'Laravel',
    manifest: '/site.webmanifest',
    themeColor: '#0f172a',
    appleTouchIcon: '/apple-touch-icon.png',
    appleWebAppStatusBarStyle: 'black',
);

这会渲染应用名称、Web 应用清单链接以及 iOS 独立模式元数据。如果提供了主题颜色、Apple 状态栏样式和 Apple 触摸图标,也会一并渲染。创建 Web 应用清单和注册 Service Worker 仍由您的应用负责。

您可以在默认配置或运行时元数据中使用 pwa 方法。路由元数据支持上述各项独立属性。

性能与发现

Laravel Head 可渲染性能提示、分页链接、语言区域备用链接和 Feed 发现:

Head::preload(asset('fonts/inter.woff2'), as: 'font', crossorigin: true)
    ->prefetch(asset('images/next.webp'))
    ->preconnect('https://cdn.example.com')
    ->dnsPrefetch('https://analytics.example.com')
    ->paginate($posts)
    ->alternates([
        'en' => 'https://example.com/en/about',
        'fr' => 'https://example.com/fr/about',
        'x-default' => 'https://example.com/about',
    ])
    ->feed('/feed', title: 'Laravel RSS')
    ->feed('/feed.atom', type: 'atom', title: 'Laravel Atom');

对于本地资源,preloadAsset() 和 prefetchAsset() 会通过 asset() 辅助函数解析 URL,并根据文件扩展名检测 as 属性。字体预加载会自动包含 crossorigin,预加载规范要求即使是同源字体也需要此属性:

Head::preloadAsset('fonts/inter.woff2')
    ->prefetchAsset('images/next.webp');
<link rel="preload" href="https://example.com/fonts/inter.woff2" as="font" crossorigin>
<link rel="prefetch" href="https://example.com/images/next.webp" as="image">

可以显式传入 as 参数以覆盖自动检测。如果无法根据扩展名检测出 as 属性,preloadAsset 方法会抛出异常,因为浏览器会忽略没有此属性的预加载;而 prefetchAsset 方法则只会省略该属性。

自定义标签

对于没有专门方法的标签,请使用 meta() 和 link():

Head::meta('format-detection', 'telephone=no')
    ->meta('article:author', $post->author->name)
    ->link('search', '/opensearch.xml', [
        'type' => 'application/opensearchdescription+xml',
        'title' => 'Laravel Search',
    ])
    ->link('me', 'https://social.example.com/@laravel');

当浏览器仅应在匹配条件下应用标签时,你可以在 meta 标签中包含媒体查询:

use Laravel\Head\Enums\Media;

Head::meta('theme-color', '#ffffff', media: Media::Light)
    ->meta('theme-color', '#111827', media: Media::Dark);

meta 方法使用 name 属性处理常规 meta 标签。对于通常使用 property 属性的键,例如 Open Graph (og:) 或文章元数据 (article:),该方法会自动切换:

Head::meta('description', 'About Laravel')
    ->meta('og:title', 'About Laravel');
<meta name="description" content="About Laravel">
<meta property="og:title" content="About Laravel">

你可以传入 property: true 或 property: false 来显式选择任一属性。

Schema

内置的 Schema 构建器涵盖了常见的 JSON-LD 类型:

use Laravel\Head\Enums\OfferAvailability;
use Laravel\Head\Facades\Schema;

Head::schema(
    Schema::product()
        ->name($product->name)
        ->offers(
            Schema::offer()
                ->price($product->price)
                ->currency('USD')
                ->availability(OfferAvailability::InStock)
        )
);

内置工厂方法有 article、blogPosting、product、offer、brand、breadcrumbs、faq、organization、person、webPage 和 webSite。未知的工厂方法会创建一个通用 Schema 对象,因此你仍可表达自定义的 schema.org 类型。

当 JSON-LD Schema 数据无效时,Laravel Head 会在非生产环境中抛出异常,并在生产环境中记录警告。

面包屑导航

面包屑项可以逐个添加,也可以批量添加。位置将按照添加项的顺序自动分配:

Head::schema(
    Schema::breadcrumbs()->items([
        'Home' => route('home'),
        'Shop' => route('shop.index'),
        'Shoes' => route('shop.category', 'shoes'),
    ])
);

你可以使用 item 方法追加单个面包屑项:

Schema::breadcrumbs()
    ->item('Home', route('home'))
    ->item('Shop', route('shop.index'));

常见问题(FAQ)

FAQ 条目遵循相同的模式。你可以使用 question 方法逐个添加,也可以使用 questions 方法批量添加:

Head::schema(
    Schema::faq()->questions([
        'What is Laravel Head?' => 'A fluent API for managing the document head.',
        'Is it free?' => 'Yes, it is open source.',
    ])
);

自定义 Schema

你可以显式注册自定义 Schema 类型:

use DateTimeInterface;
use Laravel\Head\Facades\Schema;
use Laravel\Head\Schema\SchemaObject;
use Laravel\Head\SchemaType;

#[SchemaType('JobPosting')]
class JobPosting extends SchemaObject
{
    public function title(string $title): static
    {
        return $this->set('title', $title);
    }

    public function datePosted(DateTimeInterface|string $date): static
    {
        return $this->date('datePosted', $date);
    }
}

Schema::register(JobPosting::class);

Head::schema(
    Schema::jobPosting()
        ->title('Senior Laravel Developer')
        ->datePosted(now())
);

渲染

Laravel Head 会将页面元数据解析为当前响应的标签。这些标签的渲染方式取决于你的应用技术栈。

HTML 渲染器支持 @head 指令以及 Laravel Head 通过 head 属性与 Inertia 共享的渲染元素。数组渲染器支持 Head::toArray(),适用于需要将解析后的元数据作为结构化数据的应用。

Blade

使用 @head 指令在布局的 <head> 中渲染累积的标签:

<head>
    <meta charset="utf-8">
    @head
</head>

@head 指令同步渲染,因此你应该在布局渲染之前定义页面元数据。

Livewire

Livewire 应用在其文档布局中使用相同的 @head 指令:

<head>
    @head
</head>

<body>
    {{ $slot }}

    @livewireScripts
</body>

无需进行 Livewire 特定的配置。Laravel Head 元数据按请求解析,且解析器是请求作用域的。因此,每次 wire:navigate 访问都会获取一份全新的文档,其 @head 输出反映目标路由的元数据。使用 wire:navigate 访问的页面会自动获得合适的路由、运行时和错误元数据,无需在组件层面编写 head 相关代码。

Inertia

在 Inertia 根模板中使用相同的 @head 指令,并搭配 Inertia 自身的组件:

<html>
<head>
    <meta charset="utf-8">
    @head

    @viteReactRefresh
    @vite(['resources/css/app.css', 'resources/js/app.tsx'])
    <x-inertia::head />
</head>
<body>
    <x-inertia::app />
</body>
</html>

安装 Inertia 后,Laravel Head 会自动将页面管理的 head 以渲染元素字符串数组的形式,通过每个页面对象上的 head 属性共享:

{
    "props": {
        "head": [
            "<title data-inertia=\"title\">Dashboard - Laravel</title>",
            "<meta data-inertia=\"description\" name=\"description\" content=\"Your application overview.\">"
        ]
    }
}

在应用中调用 createInertiaApp() 的地方启用 Inertia 的 serverHead 选项。该选项在 Inertia 3.5 及更高版本中可用:

createInertiaApp({
    // ...
    serverHead: true,
});

每个由页面管理的元素都有一个稳定的 data-inertia 键。@head 指令渲染初始文档,之后 Inertia 会接管这些元素,并在常规访问、即时访问以及前进和后退导航过程中保持其同步。这些元素存在于初始 HTML 响应中,因此爬虫和链接预览机器人无需执行 JavaScript 即可读取。无需客户端 <Head> 组件。

无论是否启用服务端渲染 (SSR),此功能都有效。如果你的应用有独立的 SSR 入口点,也请在那里启用 serverHead。无论顺序如何,Laravel Head 都会自动对 @head 与 <x-inertia::head /> 之间由页面管理的元素进行去重,同时保留由 JavaScript SSR 生成的其他 head 元素。

将 Laravel Head 添加到现有 Inertia 应用时,请移除 resources/js/app.tsx 和 resources/js/ssr.tsx 中所有标题回调,以便 Laravel Head 可以管理最终文档标题,并将 Inertia 的<Head> 组件所管理的标签迁移至 Laravel Head,确保两者不会定义相同元素。

部分重载响应会省略 head 属性,因此 Inertia 会保留上一次完整页面的 head。即时访问同样会保留当前 head,直到后台响应到达。如果你的应用已使用 head 属性,请在服务提供者中更改其名称:

use Laravel\Head\Facades\Head;

public function boot(): void
{
    Head::inertia(prop: '_head');
}

然后使用 serverHead: '_head' 将 Inertia 指向同一个属性。

静态 Inertia 标签

大多数标签应放在默认值、路由元数据或运行时元数据中,以便 Laravel Head 为每个页面解析正确的值。仅对于在首个 HTML 响应中渲染、且在该会话其余时间内由 Inertia 保持不变的文档标签,才应使用 Inertia 全局配置。

使用 Head::inertiaGlobals() 在服务提供者中注册它们:

use Laravel\Head\Facades\Head;
use Laravel\Head\HeadBuilder;

Head::inertiaGlobals(function (HeadBuilder $head) {
    $head
        ->viewport('width=device-width, initial-scale=1')
        ->colorScheme('light dark')
        ->icon('/favicon.svg', type: 'image/svg+xml')
        ->appleTouchIcon('/apple-touch-icon.png', sizes: '180x180')
        ->manifest('/site.webmanifest');
});

Inertia 全局变量被排除在 head 属性之外,渲染时不包含 data-inertia 归属属性,且首次响应后不会更新。这些全局变量适用于视口、配色方案、网站图标、触摸图标和清单等稳定的浏览器提示。如果某个标签是页面特定的、与 SEO 相关,或者后续可能会被覆盖,则应将其放入 defaults、路由元数据或运行时元数据中。

需要以结构化数据形式(而非渲染后的标签)获取已解析元数据的应用,可调用 Head::toArray()。返回的数据包含标题、Open Graph 值、JSON-LD 模式以及其他已解析的元数据。