This commit is contained in:
Emile Bangma 2025-12-09 02:23:58 +07:00 committed by GitHub
commit 08fa7e8ef3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 59 additions and 18 deletions

View File

@ -2,6 +2,10 @@
title: AliasRedirects title: AliasRedirects
tags: tags:
- plugin/emitter - plugin/emitter
permalink: "permalink"
alias:
- "alias"
- "aliases"
--- ---
This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files. This plugin emits HTML redirect pages for aliases and permalinks defined in the frontmatter of content files.
@ -11,14 +15,15 @@ For example, A `foo.md` has the following frontmatter
```md title="foo.md" ```md title="foo.md"
--- ---
title: "Foo" title: "Foo"
permalink: "bar"
alias: alias:
- "bar" - "baz"
--- ---
``` ```
The target `host.me/bar` will be redirected to `host.me/foo` The target `host.me/bar` and `host.me/baz` will be redirected to `host.me/foo`
Note that these are permanent redirect. Note that these are permanent redirects.
The emitter supports the following aliases: The emitter supports the following aliases:

View File

@ -1,4 +1,4 @@
import { FullSlug, isRelativeURL, resolveRelative, simplifySlug } from "../../util/path" import { FullSlug, FilePath, joinSegments, isRelativeURL, resolveRelative, simplifySlug } from "../../util/path"
import { QuartzEmitterPlugin } from "../types" import { QuartzEmitterPlugin } from "../types"
import { write } from "./helpers" import { write } from "./helpers"
import { BuildCtx } from "../../util/ctx" import { BuildCtx } from "../../util/ctx"
@ -38,17 +38,45 @@ async function* processFile(ctx: BuildCtx, file: VFile) {
export const AliasRedirects: QuartzEmitterPlugin = () => ({ export const AliasRedirects: QuartzEmitterPlugin = () => ({
name: "AliasRedirects", name: "AliasRedirects",
async *emit(ctx, content) {
for (const [_tree, file] of content) {
yield* processFile(ctx, file)
}
},
async *partialEmit(ctx, _content, _resources, changeEvents) { async *partialEmit(ctx, _content, _resources, changeEvents) {
for (const changeEvent of changeEvents) { for (const changeEvent of changeEvents) {
if (!changeEvent.file) continue if (!changeEvent.file) continue
if (changeEvent.type === "add" || changeEvent.type === "change") { if (changeEvent.type === "add" || changeEvent.type === "change") {
// add new ones if this file still exists // add new ones if this file still exists
yield* processFile(ctx, changeEvent.file) yield* processFile(ctx, changeEvent.file)
}
},
async emit(ctx, content, _resources): Promise<FilePath[]> {
const { argv } = ctx
const fps: FilePath[] = []
for (const [_tree, file] of content) {
const ogSlug = simplifySlug(file.data.slug!)
const slugs = [file.data.frontmatter?.permalink || [], ...(file.data.aliases || [])]
for (const slug of slugs ?? []) {
if (typeof slug !== "string") continue
const redirUrl = file.data.slug!
const fp = await write({
ctx,
content: `
<!DOCTYPE html>
<html lang="en-us">
<head>
<title>${ogSlug}</title>
<link rel="canonical" href="${redirUrl}">
<meta name="robots" content="noindex">
<meta charset="utf-8">
<meta http-equiv="refresh" content="0; url=${redirUrl}">
</head>
</html>
`,
slug: slug as FullSlug,
ext: ".html",
})
fps.push(fp)
} }
} }
}, },

View File

@ -3,9 +3,12 @@ import remarkFrontmatter from "remark-frontmatter"
import { QuartzTransformerPlugin } from "../types" import { QuartzTransformerPlugin } from "../types"
import yaml from "js-yaml" import yaml from "js-yaml"
import toml from "toml" import toml from "toml"
import { FilePath, FullSlug, getFileExtension, slugifyFilePath, slugTag } from "../../util/path" import { FilePath, FullSlug, getFileExtension, joinSegments, slugifyFilePath, slugTag } from "../../util/path"
import { QuartzPluginData } from "../vfile" import { QuartzPluginData } from "../vfile"
import { i18n } from "../../i18n" import { i18n } from "../../i18n"
import { Argv } from "../../util/ctx"
import { VFile } from "vfile"
export interface Options { export interface Options {
delimiters: string | [string, string] delimiters: string | [string, string]
@ -19,7 +22,10 @@ const defaultOptions: Options = {
function coalesceAliases(data: { [key: string]: any }, aliases: string[]) { function coalesceAliases(data: { [key: string]: any }, aliases: string[]) {
for (const alias of aliases) { for (const alias of aliases) {
if (data[alias] !== undefined && data[alias] !== null) return data[alias] if (data[alias] !== undefined && data[alias] !== null) {
if (typeof data[alias] === "string") return [data[alias]]
return data[alias]
}
} }
} }
@ -40,13 +46,14 @@ function coerceToArray(input: string | string[]): string[] | undefined {
.map((tag: string | number) => tag.toString()) .map((tag: string | number) => tag.toString())
} }
function getAliasSlugs(aliases: string[]): FullSlug[] { export function getAliasSlugs(aliases: string[], _argv: Argv, file: VFile): FullSlug[] {
const res: FullSlug[] = [] const slugs: FullSlug[] = aliases.map(
for (const alias of aliases) { (alias) => (alias.startsWith("/") ? alias : `/${alias}`) as FullSlug,
const isMd = getFileExtension(alias) === "md" )
const mockFp = isMd ? alias : alias + ".md" const permalink = file.data.frontmatter?.permalink
const slug = slugifyFilePath(mockFp as FilePath) if (typeof permalink === "string") {
res.push(slug) const absolutePermalink = permalink.startsWith("/") ? permalink : `/${permalink}`
slugs.push(absolutePermalink as FullSlug)
} }
return res return res
@ -145,6 +152,7 @@ declare module "vfile" {
published: string published: string
description: string description: string
socialDescription: string socialDescription: string
permalink: string
publish: boolean | string publish: boolean | string
draft: boolean | string draft: boolean | string
lang: string lang: string