Skip to content

Commit

Permalink
feat(sitemap2): update sitemap
Browse files Browse the repository at this point in the history
  • Loading branch information
Mister-Hope committed Jan 27, 2022
1 parent 6c309d8 commit 6b779ad
Show file tree
Hide file tree
Showing 10 changed files with 495 additions and 301 deletions.
202 changes: 202 additions & 0 deletions packages/sitemap2/src/node/generateSitemap.ts
@@ -0,0 +1,202 @@
import { chalk, fs, logger, withSpinner } from "@vuepress/utils";
import { SitemapStream } from "sitemap";

import type { App, Page } from "@vuepress/core";
import type { GitData } from "@vuepress/plugin-git";
import type {
ModifyTimeGetter,
SitemapFrontmatterOption,
SitemapImageOption,
SitemapLinkOption,
SitemapNewsOption,
SitemapOptions,
SitemapVideoOption,
} from "../shared";

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

const reportedLocales: string[] = [];

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

const generatePageMap = (
options: SitemapOptions,
app: App
): Map<string, SitemapPageInfo> => {
const {
changefreq = "daily",
excludeUrls = ["/404.html"],
modifyTimeGetter = ((page: Page<{ git: GitData }>): string =>
page.data.git?.updatedTime
? new Date(page.data.git.updatedTime).toISOString()
: "") as ModifyTimeGetter,
} = options;

const {
pages,
options: { base, locales },
} = app;

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 metaRobotTags = (page.frontmatter.head || []).find(
(head) => head[1].name === "robots"
);

const excludePage = metaRobotTags
? ((metaRobotTags[1].content as string) || "")
.split(/,/u)
.map((content) => content.trim())
.includes("noindex")
: frontmatterOptions.exclude;

if (excludePage) excludeUrls.push(page.path);

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

let links: SitemapLinkOption[] = [];

if (relatedLocales.length > 1) {
// warnings for missing `locale[path].lang` in debug mode
if (app.env.isDebug)
relatedLocales.forEach((localePrefix) => {
if (
!locales[localePrefix].lang &&
!reportedLocales.includes(localePrefix)
) {
logger.warn(
`[@vuepress/plugin-sitemap] 'lang' option for ${localePrefix} is missing`
);
reportedLocales.push(localePrefix);
}
});

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

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

// log sitemap info in debug mode
if (app.env.isDebug) {
logger.info(
`[@vuepress/plugin-sitemap] sitemap option for ${page.path}`,
sitemapInfo
);
}

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

options.excludeUrls = excludeUrls;

return pagesMap;
};

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

await withSpinner(`Generating sitemap to ${chalk.cyan(sitemapFilename)}`)(
() =>
new Promise<void>((resolve) => {
const sitemap = new SitemapStream({
hostname,
xmlns,
});
const pagesMap = generatePageMap(options, app);
const sitemapXMLPath = dir.dest(sitemapFilename);
const writeStream = fs.createWriteStream(sitemapXMLPath);

sitemap.pipe(writeStream);

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

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

const robotTxtPath = dir.dest("robots.txt");

if (fs.existsSync(robotTxtPath)) {
await withSpinner(`Appended sitemap path to ${chalk.cyan("robots.txt")}`)(
async () => {
const robotsTxt = await fs.readFile(robotTxtPath, { encoding: "utf8" });

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

await fs.writeFile(robotTxtPath, newRobotsTxtContent, { flag: "w" });
}
);
}
};
24 changes: 3 additions & 21 deletions packages/sitemap2/src/node/index.ts
@@ -1,24 +1,6 @@
import { generateSiteMap, logger } from "./sitemap";
import { sitemapPlugin } from "./plugin";

import type { Plugin } from "@vuepress/core";
import type { SitemapOptions } from "./types";

export * from "./types";

const sitemapPlugin: Plugin<SitemapOptions> = (options, app) => {
if (!options.hostname) {
logger.error("Required 'hostname' option is missing!");

return { name: "vuepress-plugin-sitemap2" };
}

return {
name: "vuepress-plugin-sitemap2",

async onGenerated(): Promise<void> {
await generateSiteMap(options as SitemapOptions, app);
},
};
};
export * from "./plugin";
export * from "../shared";

export default sitemapPlugin;
29 changes: 29 additions & 0 deletions packages/sitemap2/src/node/plugin.ts
@@ -0,0 +1,29 @@
import { logger } from "@vuepress/utils";
import { generateSiteMap } from "./generateSitemap";

import type { Plugin, PluginConfig, PluginObject } from "@vuepress/core";
import type { SitemapOptions } from "../shared";

export const sitemapPlugin: Plugin<SitemapOptions> = (options, app) => {
const plugin: PluginObject = {
name: "vuepress-plugin-sitemap2",
};

if (!options.hostname) {
logger.warn(`[${plugin.name}] 'hostname' is required`);

return plugin;
}

return {
...plugin,

async onGenerated(): Promise<void> {
await generateSiteMap(options as SitemapOptions, app);
},
};
};

export const sitemap = (
options: SitemapOptions
): PluginConfig<SitemapOptions> => [sitemapPlugin, options];

0 comments on commit 6b779ad

Please sign in to comment.