Skip to content
This repository has been archived by the owner on Apr 24, 2023. It is now read-only.

Commit

Permalink
feat(sitemap): rebuild sitemap
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Jun 2, 2022
1 parent ab95621 commit 498249a
Show file tree
Hide file tree
Showing 10 changed files with 305 additions and 215 deletions.
28 changes: 28 additions & 0 deletions packages/sitemap/src/node/compact/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { deprecatedLogger } from "./utils";
import type { SitemapOptions } from "../../types";

/** @deprecated */
export const covertOptions = (
options: SitemapOptions & Record<string, unknown>
): void => {
deprecatedLogger({
options,
deprecatedOption: "urls",
newOption: "extraUrls",
});
deprecatedLogger({
options,
deprecatedOption: "exclude",
newOption: "excludeUrls",
});
deprecatedLogger({
options,
deprecatedOption: "outFile",
newOption: "sitemapFilename",
});
deprecatedLogger({
options,
deprecatedOption: "dateFormatter",
newOption: "modifyTimeGetter",
});
};
1 change: 1 addition & 0 deletions packages/sitemap/src/node/compact/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./convert";
43 changes: 43 additions & 0 deletions packages/sitemap/src/node/compact/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { black, blue } from "chalk";

export interface DeprecatedLoggerOptions {
options: Record<string, unknown>;
deprecatedOption: string;
newOption: string;
msg?: string;
scope?: string;
}

export const deprecatedLogger = ({
options,
deprecatedOption,
newOption,
msg = "",
scope = "",
}: DeprecatedLoggerOptions): void => {
if (deprecatedOption in options) {
console.warn(
blue("Sitemap:"),
black.bgYellow("warn"),
`"${deprecatedOption}" is deprecated${
scope ? ` in ${scope}` : ""
}, please use "${newOption}" instead.${msg ? `\n${msg}` : ""}`
);

if (newOption.includes(".")) {
const keys = newOption.split(".");
let temp = options;

keys.forEach((key, index) => {
if (index !== keys.length - 1) {
// ensure level exists
temp[key] = temp[key] || {};

temp = temp[key] as Record<string, unknown>;
} else temp[key] = options[deprecatedOption];
});
} else options[newOption] = options[deprecatedOption];

delete options[deprecatedOption];
}
};
178 changes: 178 additions & 0 deletions packages/sitemap/src/node/generateSitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { black, blue, cyan } from "chalk";
import { createWriteStream, readFile, existsSync, writeFile } from "fs-extra";
import { relative, resolve } from "path";
import { SitemapStream } from "sitemap";

import type { Context, Page } from "@mr-hope/vuepress-types";
import type {
SitemapFrontmatterOption,
SitemapImageOption,
SitemapLinkOption,
SitemapNewsOption,
SitemapOptions,
SitemapVideoOption,
} from "../types";

interface SitemapPageInfo {
lastmod?: string;
changefreq?: string;
priority?: number;
img?: SitemapImageOption[];
video?: SitemapVideoOption[];
links?: SitemapLinkOption[];
news?: SitemapNewsOption[];
}

const stripLocalePrefix = (
page: Page
): {
// path of root locale
defaultPath: string;
// Locale path prefix of the page
pathLocale: string;
} => ({
defaultPath: page.path.replace(page._localePath, "/"),
pathLocale: page._localePath,
});

const generatePageMap = (
options: SitemapOptions,
{ base, pages, siteConfig }: Context
): Map<string, SitemapPageInfo> => {
const {
changefreq,
excludeUrls = ["/404.html"],
modifyTimeGetter = (page: Page): string =>
page.updateTimeStamp ? new Date(page.updateTimeStamp).toISOString() : "",
} = options;

const { locales = {} } = siteConfig;

const pageLocalesMap = pages.reduce(
(map, page) => {
const { defaultPath, pathLocale } = stripLocalePrefix(page);
const pathLocales = map.get(defaultPath) || [];

pathLocales.push(pathLocale);

return map.set(defaultPath, pathLocales);
},
// a map with keys of defaultPath and string[] value with pathLocales
new Map<string, string[]>()
);

const pagesMap = new Map<string, SitemapPageInfo>();

pages.forEach((page) => {
const frontmatterOptions: SitemapFrontmatterOption =
(page.frontmatter["sitemap"] as SitemapFrontmatterOption) || {};

const metaRobots = (page.frontmatter.meta || []).find(
(meta) => meta.name === "robots"
);
const excludePage = metaRobots
? (metaRobots.content || "")
.split(/,/u)
.map((content) => content.trim())
.includes("noindex")
: frontmatterOptions.exclude;

if (excludePage || excludeUrls.includes(page.path)) return;

const lastmodifyTime = modifyTimeGetter(page);
const { defaultPath } = stripLocalePrefix(page);
const relatedLocales = pageLocalesMap.get(defaultPath) || [];

let links: SitemapLinkOption[] = [];

if (relatedLocales.length > 1) {
links = relatedLocales.map((localePrefix) => ({
lang: locales[localePrefix]?.lang || "en",
url: `${base}${defaultPath
.replace(/^\//u, localePrefix)
.replace(/\/$/, "")}`,
}));
}

const sitemapInfo: SitemapPageInfo = {
...(changefreq ? { changefreq } : {}),
links,
...(lastmodifyTime ? { lastmod: lastmodifyTime } : {}),
...frontmatterOptions,
};

pagesMap.set(page.path, sitemapInfo);
});

return pagesMap;
};

export const generateSiteMap = async (
options: SitemapOptions,
context: Context
): Promise<void> => {
const { extraUrls = [], xmlNameSpace: xmlns } = options;
const hostname = options.hostname.replace(/\/$/, "");
const sitemapFilename = options.sitemapFilename
? options.sitemapFilename.replace(/^\//, "")
: "sitemap.xml";

console.log(
blue("Sitemap:"),
black.bgYellow("wait"),
"Generating sitemap..."
);

const { base, cwd, outDir } = context;
const sitemap = new SitemapStream({
hostname,
...(xmlns ? { xmlns } : {}),
});
const pagesMap = generatePageMap(options, context);
const sitemapXMLPath = resolve(outDir, sitemapFilename);
const writeStream = createWriteStream(sitemapXMLPath);

sitemap.pipe(writeStream);

pagesMap.forEach((page, path) =>
sitemap.write({
url: `${base}${path.replace(/^\//, "")}`,
...page,
})
);

extraUrls.forEach((item) =>
sitemap.write({ url: `${base}${item.replace(/^\//, "")}` })
);

await new Promise<void>((resolve) => {
sitemap.end(() => {
resolve();

console.log(
blue("Sitemap:"),
black.bgGreen("Success"),
`Sitemap generated and saved to ${cyan(relative(cwd, sitemapXMLPath))}`
);
});
});

const robotTxtPath = resolve(outDir, "robots.txt");

if (existsSync(robotTxtPath)) {
const robotsTxt = await readFile(robotTxtPath, { encoding: "utf8" });

const newRobotsTxtContent = `${robotsTxt.replace(
/^Sitemap: .*$/u,
""
)}\nSitemap: ${hostname}${base}${sitemapFilename}\n`;

await writeFile(robotTxtPath, newRobotsTxtContent, { flag: "w" });

console.log(
blue("Sitemap:"),
black.bgGreen("Success"),
`Appended sitemap path to ${cyan("robots.txt")}`
);
}
};
28 changes: 1 addition & 27 deletions packages/sitemap/src/node/index.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,3 @@
import { black, blue } from "chalk";
import { genSiteMap } from "./sitemap";

import type { Plugin } from "@mr-hope/vuepress-types";
import type { SitemapOptions } from "../types";

const sitemapPlugin: Plugin<SitemapOptions> = (options, context) => {
if (!options.hostname) {
console.log(
blue("Sitemap"),
black.bgRed("Error"),
'Not generating sitemap because required "hostname" option doesn’t exist'
);

return { name: "sitemap" };
}

return {
name: "sitemap",

async generated(): Promise<void> {
await genSiteMap(options, context);
},

plugins: [["@mr-hope/git", true]],
};
};
import { sitemapPlugin } from "./plugin";

export = sitemapPlugin;
32 changes: 32 additions & 0 deletions packages/sitemap/src/node/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { black, blue } from "chalk";
import { covertOptions } from "./compact";
import { generateSiteMap } from "./generateSitemap";

import type { Plugin, PluginOptionAPI } from "@mr-hope/vuepress-types";
import type { SitemapOptions } from "../types";

export const sitemapPlugin: Plugin<SitemapOptions> = (options, context) => {
covertOptions(options as SitemapOptions & Record<string, unknown>);

const plugin: PluginOptionAPI = {
name: "@mr-hope/vuepress-plugin-sitemap",
};

if (!options.hostname) {
console.log(
blue("Sitemap"),
black.bgRed("Error"),
'Not generating sitemap because required "hostname" option doesn’t exist'
);

return plugin;
}

return {
...plugin,

generated: async (): Promise<void> => generateSiteMap(options, context),

plugins: [["@mr-hope/git", true]],
};
};
Loading

0 comments on commit 498249a

Please sign in to comment.