Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env-dist
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
METABASE_REPO_PATH=../metabase
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Metabase docs site

An Astro site that renders the Metabase docs.

## Quick start

```sh
bun i
bun dev
```

The dev server runs at http://localhost:4321/docs/latest/.

## Serving docs from a local Metabase repo

```
cp .env-dist .env
```

Point `METABASE_REPO_PATH` at your local Metabase repo (defaults to `../metabase`, i.e. it assumes the repo is a sibling of this one).

With `METABASE_REPO_PATH` set, `/docs/latest` serves and hot-reloads files from your local Metabase repo, and only `/latest` routes are available — earlier versions 404. Comment it out to serve all versions from `./_docs` instead.

Restart the dev server after changing `.env`.
23 changes: 17 additions & 6 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
// @ts-check
import path from "node:path";
import { defineConfig } from "astro/config";
import { viteStaticCopy } from "vite-plugin-static-copy";
import { DOCS_DEST, DOCS_SRC_ROOT } from "./src/constants";
import { collectRedirects } from "./src/lib/docs/collectRedirects";
import { noopMarkdownProcessor } from "./src/lib/markdown/noopMarkdownProcessor";

// The number of leading path segments to strip from each copied file's directory.
// Computes the directory relative to the project root and strips any leading `../`.
// e.g. `_docs` -> 1, `../metabase/docs` -> 2
const docsSrcStripBase = path
.relative(process.cwd(), path.resolve(DOCS_SRC_ROOT))
.replace(/^(?:\.\.\/)+/, "")
.split("/")
.filter(Boolean).length;

// https://astro.build/config
export default defineConfig({
site: "https://www.metabase.com",
Expand All @@ -24,16 +35,16 @@ export default defineConfig({
viteStaticCopy({
targets: [
{
src: "_docs/**/*.{jpg,png,gif,json}",
dest: "docs",
rename: { stripBase: 1 }, // strips `_docs/`
src: `${DOCS_SRC_ROOT}/**/*.{jpg,png,gif,json}`,
dest: DOCS_DEST,
rename: { stripBase: docsSrcStripBase },
},
{
// TypeDoc-generated CSS/JS/icons the SDK API reference .html
// docs load via relative `assets/...` URLs.
src: "_docs/**/embedding/sdk/api/assets/*.{css,js,svg,ico}",
dest: "docs",
rename: { stripBase: 1 },
src: `${DOCS_SRC_ROOT}/**/embedding/sdk/api/assets/*.{css,js,svg,ico}`,
dest: DOCS_DEST,
rename: { stripBase: docsSrcStripBase },
},
],
}),
Expand Down
97 changes: 2 additions & 95 deletions lib/fetch-docs.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ const tar = require("tar-fs");
const gunzip = require("gunzip-maybe");

// String functions
const toTitleCase = require("titlecase");
const matter = require("gray-matter");

const { canBeProcessedByFrontmatter, squashVersion } = require("./utils.js");
const { constructDocMetadata } = require('../src/lib/docs/constructDocMetadata.ts');
const glob = require("glob");

/**
Expand Down Expand Up @@ -204,27 +204,7 @@ function buildDocsMetadata(docPath, fileContent, settings) {
return fileContent;
}

const metadata = constructDocMetadata(
docPath,
settings.majorVersion,
);
metadata.source_url = constructSourceUrl(docPath);

if (
parseInt(settings.majorVersion.split(".").pop()) > 43 ||
settings.version === "master"
) {
metadata.layout = "new-docs";
}

if (path.basename(docPath, ".md") === "README") {
metadata.permalink =
"/" +
path
.join("docs", settings.majorVersion, "index.html")
.split(path.sep)
.join("/");
}
const metadata = constructDocMetadata(docPath, settings.majorVersion);

return frontMatterify(fileContent, metadata);
}
Expand All @@ -240,79 +220,6 @@ function frontMatterify(fileContent, metadata) {
return frontmatterBlock + content;
}

function constructSourceUrl(path) {
const baseUrl = "https://github.com/metabase/metabase/blob/master/";
const source = path.split("/");
source.splice(0, 1);
return baseUrl + source.join("/");
}

function constructDocMetadata(path, version) {
const metadata = {};
metadata.version = version;
metadata.has_magic_breadcrumbs = true;
// We default to showing both category and title breadcrumbs, then toggle either a category and/or title breadcrumb in certain scenarios
// For documentation TOC pages, we _only_ use the title in the breadcrumb
// For _category_ TOC pages (one level below the root), we only use the category in the breadbrumb
metadata.show_category_breadcrumb = true;
metadata.show_title_breadcrumb = true;

// Remove prefixes to docs directory, making all paths below relative
const pathArray = path.split("/");
pathArray.splice(0, 2);

// #breadcrumb and title/category logic
if (pathArray.length === 1) {
metadata.show_category_breadcrumb = false;
metadata.category = "Table of Contents";
metadata.title = formatDocTitle(pathArray[0]);
} else {
if (pathArray[0] === "faq") metadata.category = "FAQ";
else metadata.category = toTitleCase(pathArray[0]).replace(/-/g, " ");
metadata.title = formatDocTitle(pathArray[pathArray.length - 1]);
}

// MOST categories use start.md, except for the troubleshooting guide :)
if (pathArray.length > 1 && pathArray[1].match(/^(index|start)\.md$/))
metadata.show_title_breadcrumb = false;

return metadata;
}

const ACRONYMS = [
"API",
"AWS",
"DB",
"GTAP",
"JMX",
"JWT",
"LDAP",
"RDS",
"SAML",
"SQL",
"SSL",
"SSO",
];

function formatDocTitle(filename) {
filename = filename.replace(".md", "");
filename = filename.replace(/-/g, " ");
filename = toTitleCase(filename);
return filename
.split(" ")
.map((word) => {
const wordIndex = ACRONYMS.findIndex(
(acronym) => acronym.toUpperCase() === word.toUpperCase(),
);
if (wordIndex > -1) {
return ACRONYMS[wordIndex];
}

return word;
})
.join(" ");
}

// Take newly extracted 'docs' directory, name it according
// to appropriate Electron version, copy it to electron.atom.io
// '_docs' directory and delete temp directory
Expand Down
6 changes: 2 additions & 4 deletions lib/test-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {reformatMarkdownUrls} = require('./utils.js')
const { reformatMarkdownUrls } = require('../src/lib/docs/reformatMarkdownUrls')

// Test URL reformatting

Expand Down Expand Up @@ -67,10 +67,8 @@ Anchors work [in footers][footer-anchor] too.
]

function testUrlReformatting () {
const filePath = ''

for (const test of tests) {
const actual = reformatMarkdownUrls(filePath, test.fixture)
const actual = reformatMarkdownUrls(test.fixture)
const expectedLines = test.expected.split('\n')
const actualLines = actual.split('\n')
expectedLines.forEach((eL, i) => {
Expand Down
135 changes: 2 additions & 133 deletions lib/utils.js
Original file line number Diff line number Diff line change
@@ -1,149 +1,19 @@
const MARKDOWN_LINK_REGEX = /\[(.+?)\]\((.+?)\)/gim;
const FOOTER_LINK_REGEX = /^\[(.+?)\]:\s+(.+?)\n/gim;
const fs = require("fs");
const path = require("path");
const glob = require("glob");
const matter = require("gray-matter");
const yaml = require("yamljs");
const { reformatMarkdownUrls } = require('../src/lib/docs/reformatMarkdownUrls');

function canBeProcessedByFrontmatter(filePath) {
return path.extname(filePath) === ".md" || path.extname(filePath) === ".html"
}

function reformatMarkdownUrls(filePath, body) {
let formattedBody = `${body}\n`;

// "[text](url.md?query=something#hash=else)" => "[text](url?query=something#hash=else)"
const bodyMatchesReplacements = getReplacements(
filePath,
formattedBody.match(MARKDOWN_LINK_REGEX),
);
if (bodyMatchesReplacements) {
bodyMatchesReplacements.forEach(
(replaceObj) =>
(formattedBody = formattedBody.replace(
replaceObj.match,
replaceObj.updatedMatch,
)),
);
}

// "[text]: url.md?query=something#hash=else" => "[text]: url?query=something#hash=else"
const footerMatchesReplacements = getReplacements(
filePath,
formattedBody.match(FOOTER_LINK_REGEX),
);
if (footerMatchesReplacements) {
footerMatchesReplacements.forEach(
(replaceObj) =>
(formattedBody = formattedBody.replace(
replaceObj.match,
replaceObj.updatedMatch,
)),
);
}

return formattedBody;
}

function replaceVersionInUrls(content, { version }) {
return content
.replaceAll('/latest/embedding/', `/${version}/embedding/`)
}

function extractUrl(match) {
// body
let url = match.match(/(?<=\[(.+?)\]\()(.+?)(?=\))/gim);
if (url) {
return url[0].trim();
}

// footer
url = match.match(/(?<=]: )(.+?)+/gim);
if (url) {
return url[0].trim();
}

return null;
}

function isRelativeUrl(url) {
return url.indexOf("http://") === -1 && url.indexOf("https://") === -1;
}

function isMetabaseUrl(url) {
// - metabase.com/<path>
// - <protocol>://www.metabase.com/<path>
// - <protocol>://metabase.com/<path>
return !!(
url.indexOf("metabase.") === 0 ||
url.indexOf("://www.metabase.") === 4 ||
url.indexOf("://www.metabase.") === 5 ||
url.indexOf("://metabase.") === 4 ||
url.indexOf("://metabase.") === 5
);
}

function formatUrl(url) {
// <url>.{md,markdown,html,htm}
url = url
// Remove extensions
.replace(".md", "")
.replace(".markdown", "")
.replace(".html", "")
.replace(".htm", "")
// Remove metabase.com
.replace("http://metabase.com", "")
.replace("https://metabase.com", "")
.replace("http://www.metabase.com", "")
.replace("https://www.metabase.com", "");

return url;
}

function getReplacements(filePath, matches) {
if (matches) {
return matches
.map((match) => {
const url = extractUrl(match);
if (url) {
// Relative
if (isRelativeUrl(url)) {
return {
match,
updatedMatch: match.replace(url, formatUrl(url)),
};
}
// Absolute + Metabase
else if (isMetabaseUrl(url)) {
// Has an extension?
const urlPaths = url.match(/(?<=.com)(.*)/gim);
if (urlPaths && urlPaths.length > 0) {
// Error if there's an extension
const [urlPath] = urlPaths;
if (urlPath.indexOf(".") > -1) {
console.error(
`Error:\n\t${filePath}\n\tMetabase url do not need extension: ${url}`,
);
}
}
return {
match,
updatedMatch: match.replace(url, formatUrl(url)),
};
}
} else {
console.warn(`Warning:\n\t${filePath}\n\turl not found: ${match}`);
}

return null;
})
.filter((match) => !!match);
}

return null;
}

/**
* Lops off the last point so that point releases will overwrite (i.e., update) the
* existing docs for that major release. So docs for 40.3 will update the existing docs for 40.0
Expand Down Expand Up @@ -187,7 +57,7 @@ function updateRedirectsAndLinks(dir) {
const { content, data } = matter(fileContent);
// Trim space buffering frontmatter;
// we'll add it back later in stringifyDataAndContent
let contentUpdatedLinks = reformatMarkdownUrls(filePath, content.trim());
let contentUpdatedLinks = reformatMarkdownUrls(content.trim());

if (dir !== "_docs/latest") {
contentUpdatedLinks = replaceVersionInUrls(contentUpdatedLinks, {
Expand All @@ -209,7 +79,6 @@ function updateRedirectsAndLinks(dir) {

module.exports = {
canBeProcessedByFrontmatter,
reformatMarkdownUrls,
squashVersion,
stringifyDataAndContent,
updateRedirectsAndLinks,
Expand Down
2 changes: 1 addition & 1 deletion script/docs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env node
#!/usr/bin/env bun
const program = require("commander");
const yaml = require("yamljs");
const fs = require("fs");
Expand Down
Loading