GooFonts 是一个由开发者妻子和设计师丈夫共同打造的副项目,他们两人都是排版的大粉丝。我们一直在标记 Google Fonts 并构建了一个网站,使搜索和查找合适的字体变得更容易。
GooFonts 在后端使用 WordPress,在前端使用 NuxtJS(一个 Vue.js 框架)。我很乐意与大家分享 goofonts.com 背后的故事,以及我们为该项目选择的技术以及如何调整和使用这些技术的相关技术细节。
我们为什么要创建 GooFonts
在撰写本文时,Google Fonts 提供了 977 种字体。您可以随时使用 Google Fonts 开发者 API 检查确切数量。您可以检索所有字体的动态列表,包括每个字体的可用样式和脚本列表。
Google Fonts 网站提供了一个漂亮的界面,您可以在其中预览所有字体,并按趋势、流行度、日期或名称对字体进行排序。
但是搜索功能呢?
您可以通过五类包含和排除字体:衬线、无衬线、显示、手写和等宽。
您可以在脚本中搜索(例如拉丁扩展、西里尔文或梵文(它们在 Google Fonts 中称为子集)。但您不能同时在多个子集中搜索。

您可以按四个属性进行搜索:粗细、倾斜、宽度和“样式数量”。样式(也称为变体)同时指样式(斜体或常规)和粗细(100、200,直至 900)。通常,正文字体需要三种变体:常规、粗体和斜体。“样式数量”属性筛选出具有多种变体的字体,但它不允许选择以“常规、粗体、斜体”组合出现的字体。
还有一个自定义搜索字段,您可以在其中输入查询。不幸的是,搜索仅在字体名称中执行。因此,结果通常包含来自 Google Fonts 以外服务的字体系列。

以“卡通”查询为例。它会显示来自外部铸造厂 Linotype 的“Cartoon Script”。
我记得曾经参与过一个项目,该项目需要两种高度风格化的字体——一种唤起旧西部风情,另一种模仿剧本。那一刻,我决定标记 Google Fonts。:)
GooFonts 的实际应用
让我向您展示 GooFonts 的工作原理。右侧的深色侧边栏是您的“搜索”区域。您可以在搜索字段中输入关键字——这将执行“与”搜索。例如,您可以查找既是卡通又是板状的字体。
我们精心挑选了一堆关键字——点击任意一个!如果您的项目需要一些特定的子集,请在子集部分中选中它们。您还可以检查字体所需的所有变体。
如果您喜欢某个字体,请点击其心形图标,它将存储在浏览器的 localStorage 中。您可以在 goofonts.com/bookmarks 页面上找到您的书签字体。除了代码之外,您可能还需要嵌入它们。

我们是如何构建的:WordPress 部分
首先,我们需要某种界面来预览和标记每个字体。我们还需要一个数据库来存储这些标签。
我有一些 WordPress 的经验。此外,WordPress 附带其 REST API,这为前端处理数据提供了多种可能性。这个选择很快就被确定下来了。
我选择了最简单的初始设置。每个字体都是一个帖子,我们使用帖子标签作为关键字。自定义帖子类型 也许也能奏效,但由于我们仅将 WordPress 用于数据,因此默认内容类型也能完美地工作。
显然,我们需要以编程方式添加所有字体。我们还需要能够以编程方式更新字体,包括添加新字体或添加新的可用变体和子集。
下面描述的方法可以与通过外部 API 获取的任何其他数据一起使用。在一个自定义的 WordPress 插件中,我们从 注册菜单页面 检查 API 的更新。为简单起见,页面将显示标题、激活更新的按钮和进度条以提供一些视觉反馈。
/**
* Register a custom menu page.
*/
function register_custom_menu_page() {
add_menu_page(
'Google Fonts to WordPress',
'WP GooFonts',
'manage_options',
'wp-goofonts-menu',
function() { ?>
<h1>Google Fonts API</h1>
<button type="button" id="wp-goofonts-button">Run</button>
<p id="info"></p>
<progress id="progress" max="100" value="0"></progress>
<?php }
);
}
add_action( 'admin_menu', 'register_custom_menu_page' );
让我们从编写 JavaScript 部分开始。虽然大多数使用 Ajax 与 WordPress 的示例都实现了 jQuery 和 jQuery.ajax 方法,但无需 jQuery 即可获得相同的结果,可以使用 axios 和一个小型的辅助程序 Qs.js 进行数据序列化。
我们希望在加载 axios 和 qs 之后,在页脚 加载我们的自定义脚本:
add_action( 'admin_enqueue_scripts' function() {
wp__script( 'axios', 'https://unpkg.com/axios/dist/axios.min.js' );
wp_enqueue_script( 'qs', 'https://unpkg.com/qs/dist/qs.js' );
wp_enqueue_script( 'wp-goofonts-admin-script', plugin_dir_url( __FILE__ ) . 'js/wp-goofonts.js', array( 'axios', 'qs' ), '1.0.0', true );
});
让我们看看 JavaScript 的外观
const BUTTON = document.getElementById('wp-goofonts-button')
const INFO = document.getElementById('info')
const PROGRESS = document.getElementById('progress')
const updater = {
totalCount: 0,
totalChecked: 0,
updated: [],
init: async function() {
try {
const allFonts = await axios.get('https://www.googleapis.com/webfonts/v1/webfonts?key=API_KEY&sort=date')
this.totalCount = allFonts.data.items.length
INFO.textContent = `Fetched ${this.totalCount} fonts.`
this.updatePost(allFonts.data.items, 0)
} catch (e) {
console.error(e)
}
},
updatePost: async function(els, index) {
if (index === this.totalCount) {
return
}
const data = {
action: 'goofonts_update_post',
font: els[index],
}
try {
const apiRequest = await axios.post(ajaxurl, Qs.stringify(data))
this.totalChecked++
PROGRESS.setAttribute('value', Math.round(100*this.totalChecked/this.totalCount))
this.updatePost(els, index+1)
} catch (e) {
console.error(e)
}
}
}
BUTTON.addEventListener('click', () => {
updater.init()
})
init
方法向 Google Fonts API 发出请求。一旦 API 的数据可用,我们就会调用递归异步 updatePost
方法,该方法以 POST
请求将单个字体发送到 WordPress 服务器。
现在,务必记住 WordPress 以其特定的方式实现 Ajax。首先,每个请求都必须发送到 wp-admin/admin-ajax.php
。此 URL 在管理区域中作为全局 JavaScript 变量 ajaxurl
提供。
其次,所有 WordPress Ajax 请求都必须在数据中包含 action
参数。action 的值决定了服务器端将使用哪个钩子标签。
在我们的例子中,action 的值为 goofonts_update_post
。这意味着服务器端发生的事情由 wp_ajax_goofonts_update_post
钩子决定。
add_action( 'wp_ajax_goofonts_update_post', function() {
if ( isset( $_POST['font'] ) ) {
/* the post tile is the name of the font */
$title = wp_strip_all_tags( $_POST['font']['family'] );
$variants = $_POST['font']['variants'];
$subsets = $_POST['font']['subsets'];
$category = $_POST['font']['category'];
/* check if the post already exists */
$object = get_page_by_title( $title, 'OBJECT', 'post' );
if ( NULL === $object ) {
/* create a new post and set category, variants and subsets as tags */
goofonts_new_post( $title, $category, $variants, $subsets );
} else {
/* check if $variants or $subsets changed */
goofonts_update_post( $object, $variants, $subsets );
}
}
});
function goofonts_new_post( $title, $category, $variants, $subsets ) {
$post_id = wp_insert_post( array(
'post_author' => 1,
'post_name' => sanitize_title( $title ),
'post_title' => $title,
'post_type' => 'post',
'post_status' => 'draft',
)
);
if ( $post_id > 0 ) {
/* the easy part of tagging ;) append the font category, variants and subsets (these three come from the Google Fonts API) as tags */
wp_set_object_terms( $post_id, $category, 'post_tag', true );
wp_set_object_terms( $post_id, $variants, 'post_tag', true );
wp_set_object_terms( $post_id, $subsets, 'post_tag', true );
}
}
这样,不到一分钟的时间,我们就在仪表盘中得到了近一千个帖子草稿——所有这些草稿都带有一些现成的标签。而那正是项目中至关重要、最耗时的部分开始的时候。我们需要开始手动逐个添加每个字体的标签。
在这种情况下,默认的 WordPress 编辑器没有多大意义。我们需要的是字体的预览。指向 fonts.google.com 上字体页面的链接也很有用。
自定义元框 可以很好地完成这项工作。在大多数情况下,您将使用元框作为自定义表单元素来保存一些与帖子相关的自定义数据。事实上,元框的内容实际上可以是任何 HTML。
function display_font_preview( $post ) {
/* font name, for example Abril Fatface */
$font = $post->post_title;
/* font as in url, for example Abril+Fatface */
$font_url_part = implode( '+', explode( ' ', $font ));
?>
<div class="font-preview">
<link href="<?php echo 'https://fonts.googleapis.com/css?family=' . $font_url_part . '&display=swap'; ?>" rel="stylesheet">
<header>
<h2><?php echo $font; ?></h2>
<a href="<?php echo 'https://fonts.google.com/specimen/' . $font_url_part; ?>" target="_blank" rel="noopener">Specimen on Google Fonts</a>
</header>
<div contenteditable="true" style="font-family: <?php echo $font; ?>">
<p>The quick brown fox jumps over a lazy dog.</p>
<p style="text-transform: uppercase;">The quick brown fox jumps over a lazy dog.</p>
<p>1 2 3 4 5 6 7 8 9 0</p>
<p>& ! ; ? {}[]</p>
</div>
</div>
<?php }
add_action( 'add_meta_boxes', function() {
add_meta_box(
'font_preview', /* metabox id */
'Font Preview', /* metabox title */
'display_font_preview', /* content callback */
'post' /* where to display */
);
});

标记字体是一项长期且重复的任务。它还需要高度的一致性。因此,我们首先开始定义一组标签“预设”。例如,可以是
{
/* ... */
comic: {
tags: 'comic, casual, informal, cartoon'
},
cursive: {
tags: 'cursive, calligraphy, script, manuscript, signature'
},
/* ... */
}
接下来,使用一些自定义 CSS 和 JavaScript,我们“修改”了 WordPress 编辑器和标签表单,通过添加一组预设按钮来对其进行增强。
我们是如何构建的:前端部分(使用 NuxtJS)
goofonts.com 的界面由法国平面设计师兼网页设计师 Sylvain Guizard (Sylvain Guizard,他恰好是我的丈夫) 设计。我们想要一个简单的界面,并配有一个醒目的“搜索”区域。Sylvain 刻意使用了与 Google Fonts 标识不太偏离的颜色。我们在构建独特和原创内容与避免用户混淆之间寻求平衡。
虽然我在选择后端时毫不犹豫地选择了 WordPress,但我不想在前端使用它。我们的目标是获得类似应用程序的体验,而且我个人希望使用 JavaScript 进行编码,特别是使用 Vue.js。
我偶然发现了一个使用 NuxtJS 和 WordPress 的网站示例,并决定尝试一下。这个选择很快就确定下来了。NuxtJS 是一个非常流行的 Vue.js 框架,我真的很喜欢它的简洁性和灵活性。
我尝试了不同的 NuxtJS 设置,最终构建了一个 100% 静态的网站。完全静态的解决方案感觉性能最佳;整体体验显得更加流畅。这也意味着我的 WordPress 网站仅在构建过程中使用。因此,它可以在我的本地主机上运行。这一点不容忽视,因为它消除了托管成本,最重要的是,让我可以跳过与安全相关的 WordPress 配置,并让我免受安全相关的压力。;)
如果您熟悉 NuxtJS,您可能知道完全静态生成(尚未)是 NuxtJS 的一部分。当您导航时,预渲染页面会尝试再次获取数据。
因此,我们必须以某种方式“修改”100% 静态生成。在这种情况下,我们在每个构建过程之前将获取数据的有用部分保存到一个 JSON 文件中。这得益于 Nuxt 钩子,特别是其 构建器钩子。
钩子通常用于 Nuxt 模块中
/* modules/beforebuild.js */
const fs = require('fs')
const axios = require('axios')
const sourcePath = 'http://wpgoofonts.local/wp-json/wp/v2/'
const path = 'static/allfonts.json'
module.exports = () => {
/* write data to the file, replacing the file if it already exists */
const storeData = (data, path) => {
try {
fs.writeFileSync(path, JSON.stringify(data))
} catch (err) {
console.error(err)
}
}
async function getData() {
const fetchedTags = await axios.get(`${sourcePath}tags?per_page=500`)
.catch(e => { console.log(e); return false })
/* build an object of tag_id: tag_slug */
const tags = fetchedTags.data.reduce((acc, cur) => {
acc[cur.id] = cur.slug
return acc
}, {})
/* we want to know the total number or pages */
const mhead = await axios.head(`${sourcePath}posts?per_page=100`)
.catch(e => { console.log(e); return false })
const totalPages = mhead.headers['x-wp-totalpages']
/* let's fetch all fonts */
let fonts = []
let i = 0
while (i < totalPages) {
i++
const response = await axios.get(`${sourcePath}posts?per_page=100&page=${i}`)
fonts.push.apply(fonts, response.data)
}
/* and reduce them to an object with entries like: {roboto: {name: Roboto, tags: ["clean","contemporary", ...]}} */
fonts = (fonts).reduce((acc, el) => {
acc[el.slug] = {
name: el.title.rendered,
tags: el.tags.map(i => tags[i]),
}
return acc
}, {})
/* save the fonts object to a .json file */
storeData(fonts, path)
}
/* make sure this happens before each build */
this.nuxt.hook('build:before', getData)
}
/* nuxt.config.js */
module.exports = {
// ...
buildModules: [
['~modules/beforebuild']
],
// ...
}
如您所见,我们只请求标签列表和帖子列表。这意味着我们只使用默认的 WordPress REST API 端点,不需要任何配置。
最终想法
开发 GooFonts 是一段漫长的旅程。这类项目也需要积极维护。我们定期检查 Google Fonts 以获取新的字体、子集或变体。我们会标记新项目并更新我们的数据库。最近,我非常兴奋地发现Bebas Neue加入了这个大家庭。在众多鲜为人知的字体中,我们也有一些个人偏爱的字体。
作为一名定期举办研讨会的培训师,我能够观察到真实用户使用 GooFonts 的情况。在这个项目阶段,我们希望获得尽可能多的反馈。我们希望 GooFonts 能够成为网页设计师一个实用、方便且直观的工具。待办事项之一是根据字体名称进行搜索。我们也希望能够添加分享书签集的可能性,并创建多个字体“收藏”。
作为一名开发者,我非常享受这个项目的跨学科特性。这是我第一次使用 WordPress REST API,也是我第一次使用 Vue.js 进行大型项目,并且我学到了很多关于排版方面的知识。
如果可以重来,我们会做些什么不同的事情吗?当然会。这是一个学习过程。另一方面,我认为我们不会改变主要工具。WordPress 和 Nuxt.js 的灵活性证明是正确的选择。如果今天重新开始,我肯定会花时间探索GraphQL,并且我可能会在将来实现它。
我希望您能发现一些讨论过的方法有用。正如我之前所说,您的反馈非常宝贵。如果您有任何疑问或意见,请在评论中告诉我!
看起来很棒,下次我需要找字体时一定会使用它!
我之前尝试过完全静态地使用 Nuxt,遇到了一些问题,尽管我试图在 asyncData 中请求文章/页面,将其添加到 Vuex 存储中,然后将其用于任何后续加载。我更喜欢您在构建前检索所有需要内容的解决方案,简单得多!我最终使用了 Gridsome,因为它在内部处理了这个问题,但可能会在未来的项目中再次尝试使用 Nuxt。
谢谢!看来完整的静态生成很快就会出现在 Nuxt 中 – 查看这条推文 :)
这很棒。Google Fonts 未解决的一个方面,我最近也需要用到,就是按 OpenType 特性和变体轴进行过滤。
谢谢!变体轴是什么意思?
我一直在浏览 Google Fonts(和 Adobe Fonts),并且对无法根据 OpenType 特性查找字体感到沮丧(正在寻找具有表格数字的字体)。
如果 Google 在其字体 API 中提供这些元数据,那么将其添加到网站中将是一个有价值的筛选器。
非常棒的概念和执行。标签在大多数情况下都非常直观,但“正宗”和“活泼”有点模糊。我不确定为什么 Google 没有选择原生标签,所以感谢您填补了这个空白:) 干得好。
非常感谢您的反馈!
有没有可能开源?
嗨,目前还没有,至少现在还没有:)