页面元数据

你是否曾经将页面链接粘贴到某个程序中,经过短暂的获取后,该程序在信息框中显示了关于该页面的信息?这是可能的,因为每个HTML页面都支持拥有一个<head>块,你可以在其中指定关于它的元数据。

然而,Kobweb会自动为你的网站生成HTML!那么你如何指定这些元数据呢?

有两种方式:通过构建脚本(用于应该在每个页面上都存在的元数据),以及通过使用Kotlin/JS API的代码。大多数项目会使用这两种方法,所以我们将在本文中探讨这两种方法。

站点范围的元数据

对于任何应用Kobweb Application或Library Gradle插件的构建脚本,你可以访问index块,它分别位于kobweb.appkobweb.library块下。

index块包含一个head属性,这是你可以使用Kotlinx.html DSL为页面指定元数据的地方。

让我们用一个具体的例子来演示这一点。假设你最终想要生成一个带有如下<head>块的HTML页面:

<head>
  <title>My Portfolio</title>
  <meta name="description" content="A portfolio site listing my resume, skills, and experiences." />
  <!-- Install instructions for Google Roboto font -->
  <link rel="preconnect" href="https://fonts.googleapis.com">
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
  <link href="https://fonts.googleapis.com/..." rel="stylesheet">
</head>

在你的构建脚本中:

plugins {
    alias(libs.plugins.kobweb.application)
}

kobweb {
    app {
        index {
            description.set("A portfolio site listing my resume, skills, and experiences.")
            head.addAll {
                link(rel = "preconnect", href = "https://fonts.googleapis.com")
                link(rel = "preconnect", href = "https://fonts.gstatic.com") {
                    attributes["crossorigin"] = ""
                }
                link(rel = "stylesheet", href = "https://fonts.googleapis.com/...")
            }
        }
    }
}
site/build.gradle.kts
Note

对于应用Kobweb application插件的构建脚本,Kobweb提供description属性作为便利功能,但它最终只是添加具有适当属性的<meta>标签的简写——这里是<meta name="description" content="..." />

提供它是因为它被如此普遍地使用,每个网站都应该有一个。我们还希望鼓励用户在application模块内设置他们的描述,而不是在某个library模块中,因为使用它的用户可能不会期望这样。

类似地,Kobweb application构建脚本默认也会在<head>块中添加一个icon链接,你可以使用faviconPath属性来控制它(默认为"/favicon.ico")。

我们的<head>块大部分已经完成了,但我们仍然缺少标题!这是因为标题实际上是在网站的.kobweb/conf.yaml文件中指定的。在这里,它应该看起来像:

site:
  title: My Portfolio
.kobweb/conf.yaml

有了这个,这个网站的所有Kobweb页面现在都将包含我们想要的<head>块。

URL拦截

许多流行的Web服务作为资源包(脚本、样式表和数据)提供,这些资源一起提供功能和/或样式。这些通常托管在内容分发网络(通常缩写为CDN)上。安装说明通常建议将它们作为一个简单的选项——只需在页面的<head>块中添加一个脚本标签和样式表链接,你就可以开始了!

不幸的是,这个简单的解决方案可能导致网站不符合GDPR。简而言之,这是因为对CDN的请求涉及作为交易一部分共享用户的IP地址,而GDPR不允许在未经用户同意的情况下与不遵循欧洲数据保护法的服务器共享个人数据。

Note

关于GDPR的进一步讨论远远超出了本文档的范围,但如果你感兴趣,你可能想要查看像https://en.wikipedia.org/wiki/Content_delivery_network#Security_and_privacy这样的链接,并找到更详细讨论该主题的博客文章。

避免GDPR合规问题的常见方法是自己托管资源。Kobweb旨在通过URL拦截使这尽可能简单,你可以在index块中配置它。

自动自托管

最简单的方法是让Kobweb自己处理自托管,通过选择加入:

kobweb {
    app {
        index {
            interceptUrls {
                enableSelfHosting()
            }
        }
    }
}
site/build.gradle.kts

让我们用一个真实的例子来演示这是如何工作的。假设你想要使用流行的glMatrix库为你的网站添加OpenGL支持(就像kobweb create examples/opengl所做的那样)。搜索后,你发现它托管在Cloudflare上:

kobweb {
    app {
        index {
            script {
                src = "https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.js"
            }
        }
    }
}
site/build.gradle.kts

如果你导航到你的网站并在浏览器的开发工具中打开网络选项卡,你会看到它向预期的URL发出请求。

然而,如果你选择自托管:

kobweb {
    app {
        index {
            script {
                src = "https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.js"
            }
            interceptUrls { enableSelfHosting() }
        }
    }
}
site/build.gradle.kts

并重新运行你的网站,然后你会看到gl-matrix-min.js的副本在构建时被本地下载。如果你现在检查网络选项卡,你会看到文件托管在你自己的服务器上,URL类似于https://yoursite.com/_kobweb/self-host/cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.js

自托管的一个非常好的功能是它会检查任何作为CSS文件(即样式表)的目标资源,并递归下载它引用的任何其他资源。否则,如果你只是自己下载了这样的样式表,你可能认为你已经完成了,而没有意识到你仍然在破坏GDPR合规性,因为还在发出额外的请求来获取其他资源。

Caution

一般来说,CDN链接提供了相当多的优势,所以你不应该仅仅因为容易做到就替换它们——你会增加你的带宽费用,同时可能比CDN为你的用户提供这些文件的速度慢得多。在承诺自托管之前,你应该仔细考虑你的情况和权衡。

手动拦截URL

如果你想要更多地控制哪些URL被拦截,你可以在URL拦截块中指定你自己的replace规则。

假设不是你自己指定glMatrix依赖,而是由第三方Kobweb库完成的。当你构建你的网站时,Kobweb的Gradle输出会通知你Cloudflare gl-matrix-min.js URL将被添加到你的最终页面(或者你可能在打开页面并检查其<head>块后注意到这一点)。

虽然你可以在这里使用enableSelfHosting(),但让我们展示如何进行手动拦截:

kobweb {
    app {
        index {
            interceptUrls {
                replace(
                    "https://cdnjs.cloudflare.com/ajax/libs/gl-matrix/3.4.2/gl-matrix-min.js",
                    "/assets/scripts/gl-matrix-min.js"
                )
            }
        }
    }
}
site/build.gradle.kts

在这一点上,你需要自己下载脚本文件并将其放置在正确的位置(这里是site/src/jsMain/resources/assets/scripts/gl-matrix-min.js)。但在那之后,你又在自托管了!

再次检查网络选项卡,你应该看到gl-matrix-min.js现在从你的域名提供,而不是从CDN。

页面特定的元数据

通常,你会希望在每个页面的基础上动态设置元数据。

例如,也许你不希望你的网站标题在每个页面上都显示,而是希望用页面的标题覆盖它(特别是因为这是出现在浏览器选项卡中的名称)。我们在这样做的时候,让我们也更新描述,因为两者绝对应该保持同步。

为了实现这一点,我们将使用JavaScript标准库,它提供了进入DOM并修改它的功能。使用Kotlin/JS,你可以写:

import kotlinx.browser.document

private fun Document.setPageMetadata(title: String, description: String) {
    title = title
    head!!
        .querySelector("meta[name='description']")
        .setAttribute("content", description)
}

// Later in the file...
document.setPageMetadata(
    "Understanding Metadata",
    "This is a blog post about understanding HTML metadata."
)

上面的代码实际上做了一个强假设,即在你的页面的<head>块中已经存在一个名为description<meta>标签(注意querySelector调用后的!!操作符)。

然而,如果你可能正在处理一个可能并不总是存在的head元素,或者如果你想要更防御性地编写上面的代码,你可以使用"查询或创建"模式:

private fun Document.setDescription(description: String) {
    val head = document.head!!
    (head.querySelector("meta[name='description']") ?:
        document.createElement("meta").apply {
            setAttribute("name", "description")
            head.appendChild(this)
        }
    ).setAttribute("content", description)
}

放置这样代码的一个非常自然的地方是在布局可组合函数内,通常在LaunchedEffect内:

@Layout
@Composable
fun PageLayout(ctx: PageContext, content: @Composable () -> Unit) {
    // Get title and description out of ctx.data
    LaunchedEffect(title, description) {
        document.setPageMetadata(title, description)
    }
}
Note

你可以在Kobweb网站本身中找到我们使用这种模式。

这就是全部了。你可能希望熟悉Kotlin/JS公开的querySelector方法Document接口