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
10 changes: 9 additions & 1 deletion docusaurus.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ const config: Config = {
// docs/cli/ override it per-page via `custom_edit_url` (see
// scripts/sync-docs.ts) to point at the real source instead.
editUrl: 'https://github.com/sendra-lab/sendra-web/edit/main/',
// "Last updated on <date> by <author>" under each page. The date
// comes from git history (or a `last_update` front matter
// override); the author isn't taken from git, which has more than
// one name for the same person — src/theme/LastUpdated renders a
// fixed author linking to their GitHub instead.
showLastUpdateTime: true,
// VERSIONING DECISION (v1): intentionally unversioned — a single
// "current" version, which is Docusaurus's default with no
// `versions`/`disableVersioning` config at all (nothing to set
Expand Down Expand Up @@ -123,6 +129,8 @@ const config: Config = {
xslt: true,
},
editUrl: 'https://github.com/sendra-lab/sendra-web/edit/main/',
// Same "Last updated" line as the docs (see the docs options).
showLastUpdateTime: true,
// Useful options to enforce blogging best practices
onInlineTags: 'warn',
onInlineAuthors: 'warn',
Expand Down Expand Up @@ -232,7 +240,7 @@ const config: Config = {
],
},
],
copyright: `Copyright © ${new Date().getFullYear()} Sendra. Built by <a href="https://oyibe.vercel.app" target="_blank" rel="noreferrer">Oyibe</a>.`,
copyright: `Copyright © ${new Date().getFullYear()} Sendra. Built by <a href="https://oyibe.dev" target="_blank" rel="noreferrer">Oyibe</a>.`,
},
prism: {
theme: prismThemes.github,
Expand Down
49 changes: 47 additions & 2 deletions scripts/sync-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,35 @@ async function fetchDocsFileList(): Promise<string[]> {
);
}

/**
* Date of the most recent commit (at `ref`) that touched sendra's docs/,
* used as the "Last updated" date on every synced page. It's one API call
* for the whole docs set, not one per file: a per-file date would need a
* request per doc, and the unauthenticated GitHub API allows only 60 an
* hour — enough to start failing the tree listing this sync depends on.
* The trade-off is that a page shows when the docs last changed, not when
* that page did.
*
* Best-effort: returns null (pages just show no date) rather than failing
* the sync if GitHub can't answer.
*/
async function fetchDocsLastUpdated(): Promise<string | null> {
const url = `https://api.github.com/repos/${SOURCE_OWNER}/${SOURCE_REPO}/commits?sha=${ref}&path=${SOURCE_DOCS_ROOT}&per_page=1`;
try {
const res = await fetch(url, { headers: githubHeaders() });
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
const commits = (await res.json()) as Array<{
commit: { committer: { date: string } };
}>;
return commits[0]?.commit.committer.date ?? null;
} catch (error) {
console.warn(
`Could not look up when ${SOURCE_DOCS_ROOT}/ last changed; synced pages will show no "Last updated" date: ${error}`,
);
return null;
}
}

async function fetchRawFile(sourcePath: string): Promise<string> {
const url = `https://raw.githubusercontent.com/${SOURCE_OWNER}/${SOURCE_REPO}/${ref}/${sourcePath}`;
const res = await fetch(url, token ? { headers: { Authorization: `Bearer ${token}` } } : undefined);
Expand Down Expand Up @@ -224,12 +253,20 @@ function buildFrontMatter(opts: {
title: string;
sidebarPosition?: number;
editUrl: string;
lastUpdated: string | null;
}): string {
const lines = [
"---",
`id: ${opts.id}`,
`title: "${opts.title.replace(/"/g, '\\"')}"`,
`custom_edit_url: ${opts.editUrl}`,
];
// These files are generated and untracked here, so git has no history to
// date them from; this front matter is what the "Last updated" line reads.
if (opts.lastUpdated) {
lines.push("last_update:", ` date: "${opts.lastUpdated}"`);
}
lines.push(
// sendra's docs use `{{variable}}` templating and `<https://...>`
// autolinks — valid CommonMark, not valid JSX. `mdx.format: md` opts
// just this synced file out of MDX's JSX-aware parser; the site default
Expand All @@ -238,7 +275,7 @@ function buildFrontMatter(opts: {
// top-level `format` key (see @docusaurus/mdx-loader's utils.js).
"mdx:",
" format: md",
];
);
if (opts.sidebarPosition !== undefined) {
lines.push(`sidebar_position: ${opts.sidebarPosition}`);
}
Expand Down Expand Up @@ -281,6 +318,8 @@ async function main() {
contents.set(rel, await fetchRawFile(`${SOURCE_DOCS_ROOT}/${rel}`));
}

const lastUpdated = await fetchDocsLastUpdated();

const topOrder = extractOrder(contents.get("reference.md") ?? "", ".");
const decisionsOrder = extractOrder(contents.get("decisions/README.md") ?? "", "decisions");

Expand Down Expand Up @@ -314,7 +353,13 @@ async function main() {
// copy here would be silently discarded on the next `pnpm sync-docs`.
const editUrl = `https://github.com/${SOURCE_OWNER}/${SOURCE_REPO}/edit/main/${SOURCE_DOCS_ROOT}/${rel}`;

const frontMatter = buildFrontMatter({ id, title, sidebarPosition, editUrl });
const frontMatter = buildFrontMatter({
id,
title,
sidebarPosition,
editUrl,
lastUpdated,
});
const banner =
`<!-- Auto-generated by \`pnpm sync-docs\` from ` +
`${SOURCE_OWNER}/${SOURCE_REPO}@${ref.slice(0, 12)}:${SOURCE_DOCS_ROOT}/${rel}. ` +
Expand Down
35 changes: 35 additions & 0 deletions src/theme/LastUpdated/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Wraps the stock "Last updated on <date> by <author>" line so the author is
* always the site's author, linked to their GitHub profile, instead of
* whatever name git recorded for the last commit (this repo's history has
* more than one name for the same person, and synced docs have no git
* author at all).
*
* The original component only shows an author when it's given one, so the
* link is passed in its place; it renders it inside its own <b>.
*/
import type {ReactNode} from 'react';
import LastUpdated from '@theme-original/LastUpdated';
import type LastUpdatedType from '@theme/LastUpdated';
import type {WrapperProps} from '@docusaurus/types';
import Link from '@docusaurus/Link';

type Props = WrapperProps<typeof LastUpdatedType>;

const AUTHOR_NAME = 'Oyibe Chidubem';
const AUTHOR_GITHUB_URL = 'https://github.com/dubemoyibe-star';

export default function LastUpdatedWrapper(props: Props): ReactNode {
const author = (
<Link to={AUTHOR_GITHUB_URL} title="GitHub profile">
{AUTHOR_NAME}
</Link>
);
return (
<LastUpdated
{...props}
// Typed as `string`, but the component only interpolates it into JSX.
lastUpdatedBy={author as unknown as string}
/>
);
}
Loading