feat(docs): migrate documentation site into monorepo (#5912)

This commit is contained in:
Thomas authored and GitHub committed 2026-06-11 18:48:39 +02:00
1 parent 3e464a76f2
commit 9e421c67bd
605 files changed
+26764 -219

No files matched your search

+103
View File
@@ -0,0 +1,103 @@
import { z } from 'zod';
import fs from 'fs/promises';
const sources = {
crowdin: [
{ projectId: 534422, tokenName: 'HOMARR_CROWDIN_TOKEN' },
{ projectId: 742587, tokenName: 'HOMARR_LABS_CROWDIN_TOKEN' },
],
github: [
{ repository: 'homarr', slug: 'ajnart' },
{ repository: 'homarr', slug: 'homarr-labs' },
],
};
const schema = z.object({
GITHUB_TOKEN: z.string().nonempty(),
HOMARR_CROWDIN_TOKEN: z.string().nonempty(),
HOMARR_LABS_CROWDIN_TOKEN: z.string().nonempty(),
});
const env = schema.parse(process.env);
const fetchGithubContributors = async (slug, repository) => {
const url = `https://api.github.com/repos/${slug}/${repository}/contributors?per_page=999`;
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${env.GITHUB_TOKEN}`,
Accept: 'application/vnd.github+json',
'X-GitHub-Api-Version': '2022-11-28',
},
};
const response = await fetch(url, options);
const data = await response.json();
const dataSchema = z.array(
z.object({
login: z.string(),
avatar_url: z.string().url(),
contributions: z.number(),
})
);
return dataSchema.parse(data);
};
const fetchCrowdinMembers = async (projectId, tokenName) => {
const url = `https://crowdin.com/api/v2/projects/${projectId}/members`;
const options = {
method: 'GET',
headers: {
Accept: 'application/json',
Authorization: `Bearer ${env[tokenName]}`,
},
};
const response = await fetch(url, options);
const data = await response.json();
const dataSchema = z.object({
data: z.array(
z.object({
data: z.object({
username: z.string(),
avatarUrl: z.string().url(),
}),
})
),
});
const contributionsData = dataSchema.parse(data);
return contributionsData.data.flatMap((data) => data.data);
};
const distinctBy = (callback) => (value, index, self) => {
return self.findIndex((item) => callback(item) == callback(value)) === index;
};
const githubContributors = [];
const crowdinContributors = [];
for (const { repository, slug } of sources.github) {
githubContributors.push(...(await fetchGithubContributors(slug, repository)));
}
const distinctGithubContributors = githubContributors
.filter(distinctBy((contributor) => contributor.login))
.sort((a, b) => b.contributions - a.contributions)
.map(({ contributions, ...props }) => props)
.filter((contributor) => !contributor.login.includes('[bot]'));
await fs.writeFile('./static/data/contributions.json', JSON.stringify(distinctGithubContributors));
for (const { projectId, tokenName } of sources.crowdin) {
crowdinContributors.push(...(await fetchCrowdinMembers(projectId, tokenName)));
}
const distinctCrowdinContributors = crowdinContributors.filter(
distinctBy((contributor) => contributor.username)
);
await fs.writeFile(
'./static/data/translation-contributions.json',
JSON.stringify(distinctCrowdinContributors)
);
+189
View File
@@ -0,0 +1,189 @@
const APP_ID = 'N69WSPZTID';
const API_KEY = 'b2b00f4ed8ca3dc87b5d211c55121416';
const DOCSEARCH_INDEX = 'Docusaurus';
const MARKDOWN_INDEX = 'markdown';
const MIN_DOCSEARCH_HITS = 100;
const MIN_MARKDOWN_HITS = 10;
const DOCSEARCH_FACETS = [
'docusaurus_tag',
'language',
'lang',
'version',
'type',
];
const SEARCH_CASES = [
{ term: 'docker', minHits: 1 },
{ term: 'coolify', minHits: 1 },
{ term: 'jellyseerr', minHits: 1 },
{ term: 'keyboard shortcuts', minHits: 1 },
{ term: 'version 0.12', minHits: 1 },
];
const STALE_URL_PATTERN = /\/docs\/(next|\d+(?:\.\d+)+)\//;
async function algoliaQuery(index, body) {
const response = await fetch(
`https://${APP_ID}-dsn.algolia.net/1/indexes/${index}/query`,
{
method: 'POST',
headers: {
'X-Algolia-Application-Id': APP_ID,
'X-Algolia-API-Key': API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
},
);
if (!response.ok) {
throw new Error(`Algolia query on "${index}" failed: HTTP ${response.status}`);
}
return response.json();
}
function reportLine(ok, message) {
const prefix = ok ? 'OK' : 'FAIL';
console.log(`${prefix}: ${message}`);
return ok;
}
async function resolveDocsearchIndex() {
const candidates = [DOCSEARCH_INDEX, `${DOCSEARCH_INDEX}.tmp`];
for (const index of candidates) {
const result = await algoliaQuery(index, { query: '', hitsPerPage: 0 });
const hitCount = result.nbHits ?? 0;
if (hitCount >= MIN_DOCSEARCH_HITS) {
return { index, hitCount };
}
}
const fallback = await algoliaQuery(DOCSEARCH_INDEX, { query: '', hitsPerPage: 0 });
return { index: DOCSEARCH_INDEX, hitCount: fallback.nbHits ?? 0 };
}
async function resolveMarkdownIndex() {
const candidates = [MARKDOWN_INDEX, `${MARKDOWN_INDEX}.tmp`];
for (const index of candidates) {
const result = await algoliaQuery(index, { query: '', hitsPerPage: 0 });
const hitCount = result.nbHits ?? 0;
if (hitCount >= MIN_MARKDOWN_HITS) {
return { index, hitCount };
}
}
const fallback = await algoliaQuery(MARKDOWN_INDEX, { query: '', hitsPerPage: 0 });
return { index: MARKDOWN_INDEX, hitCount: fallback.nbHits ?? 0 };
}
async function verifyDocsearchIndex(index, hitCount) {
let ok = reportLine(
hitCount >= MIN_DOCSEARCH_HITS,
`index "${index}" has ${hitCount} records (need >= ${MIN_DOCSEARCH_HITS})`,
);
const sample = await algoliaQuery(index, {
query: 'docker',
hitsPerPage: 1,
attributesToRetrieve: DOCSEARCH_FACETS,
});
const record = sample.hits?.[0] ?? {};
const recordFields = ['docusaurus_tag', 'language', 'lang', 'version', 'type'];
for (const field of recordFields) {
const present = record[field] !== undefined && record[field] !== null;
ok = reportLine(present, `records include "${field}"`) && ok;
}
for (const searchCase of SEARCH_CASES) {
const result = await algoliaQuery(index, {
query: searchCase.term,
hitsPerPage: 5,
attributesToRetrieve: ['url'],
});
const caseHits = result.nbHits ?? 0;
ok =
reportLine(
caseHits >= searchCase.minHits,
`"${searchCase.term}" returns ${caseHits} hits (need >= ${searchCase.minHits})`,
) && ok;
const staleUrls = (result.hits ?? [])
.map((hit) => hit.url)
.filter((url) => STALE_URL_PATTERN.test(url));
ok =
reportLine(
staleUrls.length === 0,
`"${searchCase.term}" has no stale versioned URLs (${staleUrls.length} stale)`,
) && ok;
}
return ok;
}
async function verifyMarkdownIndex(index, hitCount) {
let ok = reportLine(
hitCount >= MIN_MARKDOWN_HITS,
`markdown index "${index}" has ${hitCount} records (need >= ${MIN_MARKDOWN_HITS})`,
);
const coolify = await algoliaQuery(index, {
query: 'coolify',
hitsPerPage: 3,
attributesToRetrieve: ['url', 'title', 'heading', 'text'],
});
ok =
reportLine(
(coolify.nbHits ?? 0) >= 1,
`markdown index "${index}" returns coolify (${coolify.nbHits ?? 0} hits)`,
) && ok;
const sample = coolify.hits?.[0];
ok =
reportLine(
Boolean(sample?.text?.length),
`markdown records include chunked text content`,
) && ok;
return ok;
}
async function main() {
console.log('Verifying Algolia DocSearch + markdown indices...\n');
const docsearch = await resolveDocsearchIndex();
console.log(`Using DocSearch index: ${docsearch.index}\n`);
const docsearchOk = await verifyDocsearchIndex(
docsearch.index,
docsearch.hitCount,
);
console.log('');
const markdown = await resolveMarkdownIndex();
console.log(`Using markdown index: ${markdown.index}\n`);
const markdownOk = await verifyMarkdownIndex(
markdown.index,
markdown.hitCount,
);
const allOk = docsearchOk && markdownOk;
console.log('');
console.log(
allOk
? 'All checks passed.'
: 'Some checks failed — ensure the crawler config in docsearch.config.js is applied and a full recrawl has finished.',
);
process.exit(allOk ? 0 : 1);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});