commit ed5e62ade1dacc0d5fc6fda909289bcd2e78db25
parent 217d8f02cbc51db6a611184777efb49e2945f4a4
Author: LukeJ <luke.jones.2567@gmail.com>
Date: Thu, 22 Oct 2026 22:07:01 +0100
refactor: remove local projects, redirect to git pages
Diffstat:
9 files changed, 2 insertions(+), 969 deletions(-)
diff --git a/scripts/export-repos.py b/scripts/export-repos.py
@@ -1,439 +0,0 @@
-#!/usr/bin/env python3
-
-import base64
-import json
-import shlex
-import shutil
-import subprocess
-import sys
-from pathlib import Path
-
-OUTPUT_ROOT = Path("src/content/repos")
-
-def run_ssh(remote, command):
- """Run a shell command on the remote server."""
-
- result = subprocess.run(
- ["ssh", remote, command],
- text=True,
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
-
- if result.returncode != 0:
- print(
- f"SSH command failed: {command}",
- file=sys.stderr,
- )
- print(result.stderr, file=sys.stderr)
- raise RuntimeError("Remote command failed")
-
- return result.stdout
-
-
-def git(remote, repo, *args):
- """Run git --git-dir=<repo> remotely."""
-
- command = " ".join(
- [
- "git",
- "--git-dir",
- shlex.quote(repo),
- *[shlex.quote(str(arg)) for arg in args],
- ]
- )
-
- return run_ssh(remote, command)
-
-
-def get_repo_metadata(remote, repo):
- """Get basic repository metadata."""
-
- head = git(
- remote,
- repo,
- "rev-parse",
- "HEAD",
- ).strip()
-
- default_branch = git(
- remote,
- repo,
- "symbolic-ref",
- "HEAD",
- ).strip().removeprefix("refs/heads/")
-
- branches = git(
- remote,
- repo,
- "for-each-ref",
- "--format=%(refname:short)",
- "refs/heads/",
- ).splitlines()
-
- tags = git(
- remote,
- repo,
- "for-each-ref",
- "--format=%(refname:short)",
- "refs/tags/",
- ).splitlines()
-
- return {
- "head": head,
- "defaultBranch": default_branch,
- "branches": branches,
- "tags": tags,
- }
-
-
-def get_commits(remote, repo):
- """Get all commits in chronological order."""
-
- output = git(
- remote,
- repo,
- "log",
- "--reverse",
- "--format=%H%x00%P%x00%an%x00%ae%x00%aI%x00%s%x00",
- )
-
- commits = []
-
- for record in output.split("\n"):
- if not record:
- continue
-
- fields = record.split("\x00")
-
- if len(fields) < 6:
- continue
-
- sha, parents, author, email, date, message = fields[:6]
-
- commits.append(
- {
- "sha": sha,
- "shortSha": sha[:7],
- "parents": parents.split() if parents else [],
- "author": {
- "name": author,
- "email": email,
- },
- "date": date,
- "message": message,
- }
- )
-
- return commits
-
-
-def get_commit_diff(remote, repo, commit):
- """Get the complete diff for a commit."""
-
- return git(
- remote,
- repo,
- "show",
- "--format=",
- "--binary",
- commit,
- )
-
-
-def get_tree(remote, repo, commit):
- """Get the complete file tree for a commit."""
-
- result = subprocess.run(
- [
- "ssh",
- remote,
- "git",
- "--git-dir",
- repo,
- "ls-tree",
- "-r",
- "-z",
- commit,
- ],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
-
- if result.returncode != 0:
- print(result.stderr.decode(), file=sys.stderr)
- raise RuntimeError("Could not read repository tree")
-
- files = []
-
- for entry in result.stdout.split(b"\0"):
- if not entry:
- continue
-
- metadata, path = entry.split(b"\t", 1)
-
- mode, object_type, sha = metadata.decode().split()
-
- files.append(
- {
- "path": path.decode("utf-8", errors="replace"),
- "mode": mode,
- "type": object_type,
- "sha": sha,
- }
- )
-
- return files
-
-
-def get_file_content(remote, repo, commit, path):
- """Get a file's contents from a specific commit."""
-
- command = (
- f"git --git-dir {shlex.quote(repo)} "
- f"show {shlex.quote(f'{commit}:{path}')}"
- )
-
- result = subprocess.run(
- ["ssh", remote, command],
- stdout=subprocess.PIPE,
- stderr=subprocess.PIPE,
- )
-
- if result.returncode != 0:
- return None
-
- return result.stdout
-
-
-def write_json(path, data):
- """Write formatted JSON."""
-
- with open(path, "w", encoding="utf-8") as f:
- json.dump(
- data,
- f,
- ensure_ascii=False,
- indent=2,
- )
- f.write("\n")
-
-
-def export_repository(remote, repo):
- """Export a single repository."""
-
- name = Path(repo).name
-
- output = OUTPUT_ROOT / name
-
- print(f"Exporting {name}...")
-
- output.mkdir(parents=True, exist_ok=True)
- (output / "commits").mkdir(exist_ok=True)
- (output / "files").mkdir(exist_ok=True)
-
- metadata = get_repo_metadata(remote, repo)
-
- print(f" HEAD: {metadata['head']}")
- print(f" Branch: {metadata['defaultBranch']}")
- print(f" Branches: {len(metadata['branches'])}")
- print(f" Tags: {len(metadata['tags'])}")
-
- commits = get_commits(remote, repo)
-
- print(f" Commits: {len(commits)}")
-
- write_json(
- output / "repo.json",
- {
- "name": name,
- **metadata,
- "commitCount": len(commits),
- },
- )
-
- write_json(
- output / "commits.json",
- commits,
- )
-
- for number, commit in enumerate(commits, 1):
- sha = commit["sha"]
-
- print(
- f" Commit {number}/{len(commits)}: "
- f"{commit['shortSha']} {commit['message']}"
- )
-
- commit_data = {
- **commit,
- "diff": get_commit_diff(
- remote,
- repo,
- sha,
- ),
- }
-
- write_json(
- output / "commits" / f"{sha}.json",
- commit_data,
- )
-
- print(" Exporting file tree...")
-
- files = get_tree(
- remote,
- repo,
- metadata["head"],
- )
-
- write_json(
- output / "tree.json",
- files,
- )
-
- print(f" Exporting {len(files)} files...")
-
- for number, file_info in enumerate(files, 1):
- path = file_info["path"]
-
- print(
- f" File {number}/{len(files)}: {path}"
- )
-
- content = get_file_content(
- remote,
- repo,
- metadata["head"],
- path,
- )
-
- if content is None:
- continue
-
- try:
- text = content.decode("utf-8")
-
- file_data = {
- **file_info,
- "binary": False,
- "content": text,
- }
-
- except UnicodeDecodeError:
-
- file_data = {
- **file_info,
- "binary": True,
- "content": base64.b64encode(
- content
- ).decode("ascii"),
- }
-
- filename = (
- base64.urlsafe_b64encode(
- path.encode("utf-8")
- )
- .decode("ascii")
- .rstrip("=")
- )
-
- write_json(
- output / "files" / f"{filename}.json",
- file_data,
- )
-
- print(f" Finished {name}")
-
-
-def main():
- if len(sys.argv) != 2:
- print(
- f"Usage: {sys.argv[0]} user@host:/path/to/repositories",
- file=sys.stderr,
- )
- sys.exit(1)
-
- remote = sys.argv[1].split(':')[0]
- remote_root = sys.argv[1].split(':')[1]
-
- print("Checking SSH connection...")
-
- run_ssh(remote, "true")
-
- print("Finding repositories...")
-
- repos = run_ssh(
- remote,
- (
- f"find {shlex.quote(remote_root)} "
- "-mindepth 1 "
- "-maxdepth 1 "
- "-type d "
- "-print"
- ),
- ).splitlines()
-
- repos = [
- repo.strip()
- for repo in repos
- if repo.strip()
- ]
-
- if not repos:
- print(
- "No repositories found.",
- file=sys.stderr,
- )
- sys.exit(1)
-
- print(f"Found {len(repos)} repositories.")
-
- OUTPUT_ROOT.mkdir(
- parents=True,
- exist_ok=True,
- )
-
- current_names = {
- Path(repo).name
- for repo in repos
- }
-
- for existing in OUTPUT_ROOT.iterdir():
- if (
- existing.is_dir()
- and existing.name not in current_names
- ):
- print(
- f"Removing old repository: "
- f"{existing.name}"
- )
- shutil.rmtree(existing)
-
- for repo in sorted(repos):
- export_repository(
- remote,
- repo,
- )
-
- print()
- print("Repository export complete.")
-
-
-if __name__ == "__main__":
- try:
- main()
-
- except KeyboardInterrupt:
- print(
- "\nCancelled.",
- file=sys.stderr,
- )
- sys.exit(130)
-
- except Exception as e:
- print(
- f"\nERROR: {e}",
- file=sys.stderr,
- )
- sys.exit(1)-
\ No newline at end of file
diff --git a/src/pages/index.astro b/src/pages/index.astro
@@ -9,8 +9,8 @@ const title = 'Home'
const mainButtonRow = [
{
title: 'Projects',
- desc: 'A list of a few of my projects',
- link: '/projects',
+ desc: 'A few of my projects',
+ link: 'https://git.lukeonline.net/',
},
{
title: 'Blog',
diff --git a/src/pages/projects/[name].astro b/src/pages/projects/[name].astro
@@ -1,70 +0,0 @@
----
-import Base from '../../layouts/Base.astro'
-
-import { getCollection } from 'astro:content'
-import { getFileByPath, getTree } from '../../lib/repos'
-import {
- BUTTON_STYLING_SHARED,
- DISABLED_BUTTON_STYLING_SHARED,
-} from '../../consts.ts'
-import CodeContainer from '../../components/CodeContainer.astro'
-import { encodeFilePath } from '../../lib/repos.ts'
-
-export async function getStaticPaths() {
- const repos = await getCollection('repos')
-
- return repos.map((repo) => ({
- params: {
- name: repo.data.name,
- },
- props: {
- repo,
- },
- }))
-}
-
-const { repo } = Astro.props
-
-const tree = getTree(repo.data.name)
-
-function getReadMeContent() {
- const readme = tree.find((file) => file.path.toLowerCase() === 'readme.md')
-
- if (!readme) {
- return 'No README provided in this repository.'
- }
-
- const file = getFileByPath(repo.data.name, readme.path)
-
- return file.binary ? '' : file.content
-}
----
-
-<Base title={repo.data.name}>
- <div class="flex flex-row items-baseline gap-2">
- <code class="text-sm"
- ><a href={`/projects/${repo.data.name}`}>{repo.data.defaultBranch}</a
- ></code
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/commits`}
- >{repo.data.commitCount} commits</a
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/files`}>{tree.length} files</a
- >
- <a
- class={DISABLED_BUTTON_STYLING_SHARED +
- ' h-8 p-1 pl-2 items-center gap-3'}
- aria-disabled="true">README</a
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`}
- >LICENSE</a
- >
- </div>
- <CodeContainer code={getReadMeContent()} />
-</Base>
diff --git a/src/pages/projects/[name]/commit/[sha].astro b/src/pages/projects/[name]/commit/[sha].astro
@@ -1,38 +0,0 @@
----
-import { getCollection } from 'astro:content'
-import { getCommit, getCommits } from '../../../../lib/repos'
-import Base from '../../../../layouts/Base.astro'
-import CodeContainer from '../../../../components/CodeContainer.astro'
-
-export async function getStaticPaths() {
- const repos = await getCollection('repos')
-
- return repos.flatMap((repo) => {
- const commits = getCommits(repo.data.name)
-
- return commits.map((commit) => ({
- params: {
- name: repo.data.name,
- sha: commit.sha,
- },
- props: {
- repo,
- commit: getCommit(repo.data.name, commit.sha),
- },
- }))
- })
-}
-
-const { repo, commit } = Astro.props
----
-
-<Base title={`${repo.data.name} - commit ${commit.shortSha}`}>
- <div class="flex flex-col text-sm">
- <code>{commit.message}</code>
- <code>{commit.sha}</code>
- <code>{commit.author.name}</code>
- <code><{commit.author.email}></code>
- <code>{commit.date}</code>
- </div>
- <CodeContainer code={commit.diff} />
-</Base>
diff --git a/src/pages/projects/[name]/commits/index.astro b/src/pages/projects/[name]/commits/index.astro
@@ -1,97 +0,0 @@
----
-import Base from '../../../../layouts/Base.astro'
-
-import { getCollection } from 'astro:content'
-import { getCommits, getTree } from '../../../../lib/repos'
-import {
- BUTTON_STYLING_SHARED,
- DISABLED_BUTTON_STYLING_SHARED,
-} from '../../../../consts.ts'
-import { encodeFilePath } from '../../../../lib/repos.ts'
-
-export async function getStaticPaths() {
- const repos = await getCollection('repos')
-
- return repos.map((repo) => ({
- params: {
- name: repo.data.name,
- },
- props: {
- repo,
- },
- }))
-}
-
-const { repo } = Astro.props
-const { name } = Astro.params
-
-const commits = getCommits(name!)
-const tree = getTree(name!)
----
-
-<Base title={`${name}`}>
- <div class="flex flex-row items-baseline gap-2">
- <code class="text-sm"
- ><a href={`/projects/${name}`}>{repo.data.defaultBranch}</a></code
- >
-
- <a
- class={DISABLED_BUTTON_STYLING_SHARED +
- ' h-8 p-1 pl-2 items-center gap-3'}
- aria-disabled="true"
- >
- {repo.data.commitCount} commits
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${name}/files`}
- >
- {tree.length} files
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${name}/file/${encodeFilePath('README.md')}`}
- >
- README
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${name}/file/${encodeFilePath('LICENSE')}`}
- >
- LICENSE
- </a>
- </div>
-
- <table class="w-full">
- <thead>
- <tr>
- <th class="text-left">sha</th>
- <th class="text-left">Message</th>
- <th class="text-left">Date</th>
- </tr>
- </thead>
-
- <tbody>
- {
- [...commits].reverse().map((commit) => (
- <tr>
- <td class="max-w-12 min-w-12">
- <a href={`/projects/${name}/commit/${commit.sha}`}>
- <code class="text-sm">{commit.shortSha}</code>
- </a>
- </td>
-
- <td class="max-w-72 overflow-hidden text-ellipsis whitespace-nowrap">
- <code class="text-sm">{commit.message}</code>
- </td>
-
- <td>{new Date(commit.date).toLocaleDateString()}</td>
- </tr>
- ))
- }
- </tbody>
- </table>
-</Base>
diff --git a/src/pages/projects/[name]/file/[...path].astro b/src/pages/projects/[name]/file/[...path].astro
@@ -1,87 +0,0 @@
----
-import Base from '../../../../layouts/Base.astro'
-
-import { getCollection } from 'astro:content'
-import {
- decodeFilePath,
- encodeFilePath,
- getFileByPath,
- getTree,
-} from '../../../../lib/repos'
-import CodeContainer from '../../../../components/CodeContainer.astro'
-import { BUTTON_STYLING_SHARED } from '../../../../consts.ts'
-
-export async function getStaticPaths() {
- const repos = await getCollection('repos')
-
- return repos.flatMap((repo) => {
- const tree = getTree(repo.data.name)
-
- return tree
- .filter((file) => file.path)
- .map((file) => ({
- params: {
- name: repo.data.name,
- path: encodeFilePath(file.path),
- },
- props: {
- repo,
- file,
- tree,
- },
- }))
- })
-}
-
-const { repo, file } = Astro.props
-
-const tree = getTree(repo.data.name)
-const filePath = decodeFilePath(Astro.params.path!)
-const fileData = getFileByPath(repo.data.name, filePath)
----
-
-<Base title={`${repo.data.name} - ${file.path}`}>
- <div class="flex flex-row items-baseline gap-2">
- <code class="text-sm">
- <a href={`/projects/${repo.data.name}`}>
- {repo.data.defaultBranch}
- </a>
- </code>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/commits`}
- >
- {repo.data.commitCount} commits
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/files`}
- >
- {tree.length} files
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('README.md')}`}
- >
- README
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`}
- >
- LICENSE
- </a>
- </div>
-
- {
- fileData.binary ? (
- <CodeContainer code={'Binary File'} />
- ) : (
- <CodeContainer code={fileData.content} />
- )
- }
-</Base>
diff --git a/src/pages/projects/[name]/files/[...path].astro b/src/pages/projects/[name]/files/[...path].astro
@@ -1,125 +0,0 @@
----
-import { getCollection } from 'astro:content'
-import { getTree, getDirectoryChildren } from '../../../../lib/repos'
-import Base from '../../../../layouts/Base.astro'
-import { BUTTON_STYLING_SHARED } from '../../../../consts.ts'
-import { encodeFilePath } from '../../../../lib/repos.ts'
-
-export async function getStaticPaths() {
- const repos = await getCollection('repos')
-
- return repos.flatMap((repo) => {
- const tree = getTree(repo.data.name)
- const directories = new Set<string>()
-
- for (const file of tree) {
- const parts = file.path.split('/')
-
- for (let i = 1; i < parts.length; i++) {
- directories.add(parts.slice(0, i).join('/'))
- }
- }
-
- return [...directories].map((path) => ({
- params: {
- name: repo.data.name,
- path,
- },
- props: {
- repo,
- tree,
- },
- }))
- })
-}
-
-const { repo, tree } = Astro.props
-const directory = Astro.params.path ?? ''
-
-const { directories, files } = getDirectoryChildren(tree, directory)
-
-const parent = directory.includes('/')
- ? directory.slice(0, directory.lastIndexOf('/'))
- : ''
----
-
-<Base title={`${repo.data.name} - ${directory}`}>
- <div class="flex flex-row items-baseline gap-2">
- <code class="text-sm">
- <a href={`/projects/${repo.data.name}`}>
- {repo.data.defaultBranch}
- </a>
- </code>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/commits`}
- >
- {repo.data.commitCount} commits
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/files`}
- >
- {tree.length} files
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('README.md')}`}
- >
- README
- </a>
-
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`}
- >
- LICENSE
- </a>
- </div>
-
- <div
- class="flex flex-col rounded-sm p-2 font-mono text-sm whitespace-pre-line ring inset-shadow-sm ring-gray-600 inset-shadow-gray-400"
- >
- <a
- class="p-0.5 hover:text-blue-800 focus:underline"
- href={parent
- ? `/projects/${repo.data.name}/files/${parent}`
- : `/projects/${repo.data.name}/files`}
- >
- ../
- </a>
-
- {
- directories.map((child) => {
- const childPath = directory ? `${directory}/${child}` : child
-
- return (
- <a
- class="p-0.5 hover:text-blue-800 focus:underline"
- href={`/projects/${repo.data.name}/files/${childPath}`}
- >
- {child}/
- </a>
- )
- })
- }
-
- {
- files.map((file) => {
- const filePath = directory ? `${directory}/${file}` : file
-
- return (
- <a
- class="p-0.5 hover:text-blue-800 focus:underline"
- href={`/projects/${repo.data.name}/file/${encodeFilePath(filePath)}`}
- >
- {file}
- </a>
- )
- })
- }
- </div>
-</Base>
diff --git a/src/pages/projects/[name]/files/index.astro b/src/pages/projects/[name]/files/index.astro
@@ -1,92 +0,0 @@
----
-import { getCollection } from 'astro:content'
-import { getTree } from '../../../../lib/repos'
-import Base from '../../../../layouts/Base.astro'
-import {
- BUTTON_STYLING_SHARED,
- DISABLED_BUTTON_STYLING_SHARED,
-} from '../../../../consts.ts'
-import { encodeFilePath } from '../../../../lib/repos.ts'
-
-export async function getStaticPaths() {
- const repos = await getCollection('repos')
-
- return repos.map((repo) => ({
- params: {
- name: repo.data.name,
- },
- props: {
- repo,
- },
- }))
-}
-
-const { repo } = Astro.props
-
-const tree = getTree(repo.data.name)
-
-const files = tree
- .filter((file) => !file.path.includes('/'))
- .sort((a, b) => a.path.localeCompare(b.path))
-
-const directories = [
- ...new Set(
- tree
- .filter((file) => file.path.includes('/'))
- .map((file) => file.path.split('/')[0])
- ),
-].sort((a, b) => a.localeCompare(b))
----
-
-<Base title={repo.data.name}>
- <div class="flex flex-row items-baseline gap-2">
- <code class="text-sm"
- ><a href={`/projects/${repo.data.name}`}>{repo.data.defaultBranch}</a
- ></code
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/commits`}
- >{repo.data.commitCount} commits</a
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/files`}>{tree.length} files</a
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('README.md')}`}
- >README</a
- >
- <a
- class={BUTTON_STYLING_SHARED + ' h-8 p-1 pl-2 items-center gap-3'}
- href={`/projects/${repo.data.name}/file/${encodeFilePath('LICENSE')}`}
- >LICENSE</a
- >
- </div>
- <div
- class="flex flex-col rounded-sm p-2 font-mono text-sm whitespace-pre-line ring inset-shadow-sm ring-gray-600 inset-shadow-gray-400"
- >
- {
- directories.map((directory) => (
- <a
- class="p-0.5 hover:text-blue-800 focus:underline"
- href={`/projects/${repo.data.name}/files/${directory}`}
- >
- {directory}/
- </a>
- ))
- }
-
- {
- files.map((file) => (
- <a
- class="p-0.5 hover:text-blue-800 focus:underline"
- href={`/projects/${repo.data.name}/file/${encodeFilePath(file.path)}`}
- >
- {file.path}
- </a>
- ))
- }
- </div>
-</Base>
diff --git a/src/pages/projects/index.astro b/src/pages/projects/index.astro
@@ -1,18 +0,0 @@
----
-import ContentList from '../../layouts/ContentList.astro'
-
-import { getCollection, getEntry } from 'astro:content'
-
-const title = 'Projects'
-
-const repos = await getCollection('repos')
-
-const repoUrl = '/projects/'
-
-const contentData = repos.map((repo) => ({
- title: `${repo.data.name}`,
- link: `${repoUrl}${repo.data.name}`,
-}))
----
-
-<ContentList title={title} content={contentData} />