diff --git a/astro.config.mjs b/astro.config.mjs
index 5ec547d..5a62ed9 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -4,6 +4,7 @@ import tailwindcss from "@tailwindcss/vite";
import remarkMath from 'remark-math';
import rehypeKatex from 'rehype-katex';
import mdx from "@astrojs/mdx";
+import rehypeExternalLinks from "rehype-external-links";
import sitemap from "@astrojs/sitemap";
import icon from "astro-icon";
import path from "node:path";
@@ -41,7 +42,17 @@ export default defineConfig({
},
integrations: [
- mdx(),
+ // MDX is only used by the /docs guides: links to GitHub and other sites open in a new tab
+ // (announced to screen readers), so the guide stays open beside the file being edited.
+ mdx({
+ rehypePlugins: [
+ [rehypeExternalLinks, {
+ target: '_blank',
+ rel: ['noopener'],
+ content: { type: 'element', tagName: 'span', properties: { className: ['sr-only'] }, children: [{ type: 'text', value: ' (opens in a new tab)' }] },
+ }],
+ ],
+ }),
preact(),
icon(),
sitemap({
diff --git a/package-lock.json b/package-lock.json
index 56cf098..c9aa926 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,7 @@
"daisyui": "^5.5.14",
"katex": "^0.16.47",
"preact": "^10.28.1",
+ "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"tailwindcss": "^4.1.18"
@@ -4078,6 +4079,18 @@
"url": "https://github.com/sponsors/brc-dd"
}
},
+ "node_modules/is-absolute-url": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-4.0.1.tgz",
+ "integrity": "sha512-/51/TKE88Lmm7Gc4/8btclNXWS+g50wXhYJq8HWIBAGUBnoAdRu1aXeh364t/O7wXDAcTJDP8PNuNKWUDWie+A==",
+ "license": "MIT",
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-alphabetical": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -6158,6 +6171,24 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/rehype-external-links": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/rehype-external-links/-/rehype-external-links-3.0.0.tgz",
+ "integrity": "sha512-yp+e5N9V3C6bwBeAC4n796kc86M4gJCdlVhiMTxIrJG5UHDMh+PJANf9heqORJbt1nrCbDwIlAZKjANIaVBbvw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/hast": "^3.0.0",
+ "@ungap/structured-clone": "^1.0.0",
+ "hast-util-is-element": "^3.0.0",
+ "is-absolute-url": "^4.0.0",
+ "space-separated-tokens": "^2.0.0",
+ "unist-util-visit": "^5.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/unified"
+ }
+ },
"node_modules/rehype-katex": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz",
diff --git a/package.json b/package.json
index 1005bbf..8551f73 100644
--- a/package.json
+++ b/package.json
@@ -25,6 +25,7 @@
"daisyui": "^5.5.14",
"katex": "^0.16.47",
"preact": "^10.28.1",
+ "rehype-external-links": "^3.0.0",
"rehype-katex": "^7.0.1",
"remark-math": "^6.0.0",
"tailwindcss": "^4.1.18"
diff --git a/src/components/ProfileFormLink.astro b/src/components/ProfileFormLink.astro
new file mode 100644
index 0000000..513f0b0
--- /dev/null
+++ b/src/components/ProfileFormLink.astro
@@ -0,0 +1,67 @@
+---
+// "Use the profile form" box for the documentation guides (forms.profile in src/data/site.ts).
+import Button from "./ui/Button.astro";
+import Icon from "./ui/Icon.astro";
+import Todo from "./ui/Todo.astro";
+import { forms, site } from "../data/site";
+---
+
+
+
+
+
No GitHub account needed
+
+ {
+ forms.profile ? (
+ "Add or update your profile, photo and projects with one form."
+ ) : (
+ <>
+ Ask the MARC website maintainers for the profile form link at{" "}
+
+ {site.email}
+
+ .
+ >
+ )
+ }
+
+
+ {
+ forms.profile && (
+
+ )
+ }
+
+{!forms.profile && Set forms.profile in src/data/site.ts to the form's share link (marc_people/google-form/README.md, step 7).}
+
+
diff --git a/src/data/site.ts b/src/data/site.ts
index 8bd7973..404e6ff 100644
--- a/src/data/site.ts
+++ b/src/data/site.ts
@@ -130,8 +130,14 @@ export const contentRepos = {
website: "https://github.com/pdnMARC/pdnmarc.github.io",
};
+// Google Forms used by the documentation. Set `profile` to the form's share link once it exists
+// (marc_people/google-form/README.md, step 7); until then the guides ask people to contact MARC.
+export const forms = {
+ profile: "https://docs.google.com/forms/d/e/1FAIpQLSeXOCqNmVEKqgairmF4IRnbU388iqUDekWeDIOYbHRi7tNWUQ/viewform" as string | null,
+};
+
export const docs = [
- { href: "/docs/profiles/", label: "People profiles", icon: "material-symbols:person-outline", text: "Add yourself or update your photo, role and biography." },
+ { href: "/docs/profiles/", label: "People profiles", icon: "material-symbols:person-outline", text: "Use the profile form, or edit on GitHub: photo, role, biography and links." },
{ href: "/docs/projects/", label: "Projects", icon: "material-symbols:science-outline", text: "Add a project, its team, cover image and description." },
{ href: "/docs/news/", label: "News & vacancies", icon: "material-symbols:newspaper", text: "Post a news article, a vacancy with a deadline, or a social feed item." },
{ href: "/docs/publications/", label: "Publications", icon: "material-symbols:menu-book-outline", text: "Add papers to the publications sheet and link them to projects and people." },
diff --git a/src/layouts/DocsLayout.astro b/src/layouts/DocsLayout.astro
index 91dce56..53da09f 100644
--- a/src/layouts/DocsLayout.astro
+++ b/src/layouts/DocsLayout.astro
@@ -47,6 +47,10 @@ const current = Astro.url.pathname.replace(/\/?$/, "/");
+
+
diff --git a/src/pages/docs/index.astro b/src/pages/docs/index.astro
index 5024826..3e320e1 100644
--- a/src/pages/docs/index.astro
+++ b/src/pages/docs/index.astro
@@ -5,7 +5,7 @@ import Icon from "../../components/ui/Icon.astro";
import { docs, contentRepos, site } from "../../data/site";
const where = [
- { what: "People profiles", place: "marc_people repository, profiles/ folder", href: contentRepos.people, guide: "/docs/profiles/" },
+ { what: "People profiles", place: "Profile form, or marc_people repository, profiles/ folder", href: contentRepos.people, guide: "/docs/profiles/" },
{ what: "Projects", place: "marc_projects repository, projects/ folder", href: contentRepos.projects, guide: "/docs/projects/" },
{ what: "News articles and vacancies", place: "marc_news repository, articles/ folder", href: contentRepos.news, guide: "/docs/news/" },
{ what: "Social feed items", place: "Website data Google Sheet, News tab", guide: "/docs/news/#social-feed-items" },
@@ -18,8 +18,8 @@ const tasks = [
{
title: "A new member joins MARC and a project",
steps: [
- { where: "marc_people", href: "/docs/profiles/#add-a-new-profile", text: "add their profile, with their email and name, plus any other addresses (otherEmails) and the forms of their name used in their papers (otherNames)." },
- { where: "marc_projects", href: "/docs/profiles/#add-yourself-to-your-projects", text: "add the same email to each project they work on." },
+ { where: "Profile form", href: "/docs/profiles/#use-the-profile-form", text: "they fill it in once: profile, photo, other emails and names, and the projects they work on." },
+ { where: "Or on GitHub", href: "/docs/profiles/#add-a-new-profile", text: "add the profile in marc_people and their email to each project file in marc_projects." },
{ where: "Their papers", href: "/docs/publications/#linking-papers-to-people", text: "check each paper appears on their profile; if not, add the author-list form of their name to otherNames." },
],
},
@@ -32,13 +32,13 @@ const tasks = [
{
title: "Someone's email address changes",
steps: [
- { where: "marc_people", href: "/docs/profiles/#changing-your-email-or-name", text: "put the new address in email and move the old one to otherEmails. Project files stay as they are." },
+ { where: "Profile form", href: "/docs/profiles/#use-the-profile-form", text: "enter the new address under Other email addresses. Project files stay as they are." },
],
},
{
title: "Someone leaves MARC",
steps: [
- { where: "marc_people", href: "/docs/profiles/#when-you-leave-marc", text: "set status: past and, if known, currentPosition. Don't delete the profile." },
+ { where: "Profile form", href: "/docs/profiles/#use-the-profile-form", text: "choose Past member and say where they are now (or set status: past on GitHub). Don't delete the profile." },
{ where: "marc_projects", href: "/docs/profiles/#when-you-leave-marc", text: "nothing to change: they stay on their projects, marked as a past member." },
],
},
diff --git a/src/pages/docs/profiles.mdx b/src/pages/docs/profiles.mdx
index bd232c0..ca0b089 100644
--- a/src/pages/docs/profiles.mdx
+++ b/src/pages/docs/profiles.mdx
@@ -5,10 +5,34 @@ description: Add yourself to the People page, or update your photo, role and bio
---
import Callout from "../../components/ui/Callout.astro";
+import ProfileFormLink from "../../components/ProfileFormLink.astro";
-Each person at MARC keeps their own profile up to date. Profiles live in the
-[marc_people repository](https://github.com/pdnMARC/marc_people) on GitHub: one Markdown file per person
-in the `profiles/` folder, with photos in `profiles/photos/`.
+Each person at MARC keeps their own profile up to date, either with the **profile form** (no GitHub
+account needed) or by editing it on GitHub.
+
+## Use the profile form
+
+
+
+The form adds a new profile or updates yours, and keeps every link intact:
+
+- **Updating?** Fill in only what should change. Blank answers keep what is already there.
+- **Other email addresses and other forms of your name** are added to your profile, never removed, so
+ projects and papers that use them stay linked to you.
+- **Photo:** upload a JPG, PNG or WebP image. It is resized and its camera and location data removed.
+- **Projects:** tick the projects you work on and you are added to their teams.
+- **Leaving MARC?** Choose *Past member* and say where you are now.
+
+Every night at midnight (Sri Lanka time) your answers become a change request. It goes live once a MARC
+maintainer approves it. If something can't be used, for example an update from an address that is not in
+any profile, a maintainer will be in touch.
+
+The rest of this guide is for editing on GitHub, and explains what each field means.
+
+## Edit on GitHub
+
+Profiles live in the [marc_people repository](https://github.com/pdnMARC/marc_people) on GitHub: one
+Markdown file per person in the `profiles/` folder, with photos in `profiles/photos/`.
```text
marc_people/
diff --git a/src/pages/docs/site.mdx b/src/pages/docs/site.mdx
index 85f87f1..df7051d 100644
--- a/src/pages/docs/site.mdx
+++ b/src/pages/docs/site.mdx
@@ -26,6 +26,24 @@ A few values here point at content in the other repositories and must match it e
+## Profile form
+
+MARC members can add or update their profile with a Google Form instead of GitHub. Every night at 00:00
+Sri Lanka time, the **Profile Form Sync** workflow in marc_people and in marc_projects reads the form's
+response sheet and opens pull requests:
+
+- in **marc_people**, one per response, with the new or updated profile and its photo;
+- in **marc_projects**, adding the person to the projects they ticked.
+
+Responses that can't be used become an issue instead. Each pull request lists what changed, who sent it
+and the validator result, with a short review checklist. **Merge** to publish or **close** to reject;
+a response is never proposed twice. To process new responses straight away, run the workflow from the
+repository's **Actions** tab.
+
+Setup (the form, sharing, the `GOOGLE_FORM_CSV_LINK` secret) and maintenance are described in
+[marc_people/google-form/README.md](https://github.com/pdnMARC/marc_people/blob/main/google-form/README.md).
+Once the form exists, put its share link in `forms.profile` in `src/data/site.ts` so the guides show it.
+
## Where things are
| What | Where |
diff --git a/src/scripts/copy-code.ts b/src/scripts/copy-code.ts
new file mode 100644
index 0000000..41abb83
--- /dev/null
+++ b/src/scripts/copy-code.ts
@@ -0,0 +1,55 @@
+// "Copy" buttons for the code blocks in the documentation guides (DocsLayout). Folder trees
+// (```text) are left alone. Added by script, so without JavaScript the blocks are simply shown.
+const blocks = document.querySelectorAll(".docs__body pre[data-language]:not([data-language='text'])");
+
+async function copyText(text: string): Promise {
+ try {
+ await navigator.clipboard.writeText(text);
+ return true;
+ } catch {
+ // Older browsers / insecure contexts: copy through a temporary selection.
+ const area = document.createElement("textarea");
+ area.value = text;
+ area.setAttribute("readonly", "");
+ area.style.position = "fixed";
+ area.style.opacity = "0";
+ document.body.append(area);
+ area.select();
+ const ok = document.execCommand("copy");
+ area.remove();
+ return ok;
+ }
+}
+
+blocks.forEach((pre) => {
+ const wrap = document.createElement("div");
+ wrap.className = "code-copy";
+ pre.before(wrap);
+ wrap.append(pre);
+
+ const button = document.createElement("button");
+ button.type = "button";
+ button.className = "code-copy__btn";
+ button.setAttribute("aria-label", "Copy code to clipboard");
+ button.innerHTML =
+ 'Copy';
+ const label = button.querySelector("span")!;
+ const status = document.createElement("span");
+ status.className = "sr-only";
+ status.setAttribute("role", "status");
+
+ let timer: number | undefined;
+ button.addEventListener("click", async () => {
+ const ok = await copyText(pre.querySelector("code")?.textContent ?? pre.textContent ?? "");
+ label.textContent = ok ? "Copied" : "Copy failed";
+ status.textContent = ok ? "Code copied to clipboard" : "Could not copy the code";
+ button.classList.toggle("is-done", ok);
+ window.clearTimeout(timer);
+ timer = window.setTimeout(() => {
+ label.textContent = "Copy";
+ status.textContent = "";
+ button.classList.remove("is-done");
+ }, 2000);
+ });
+ wrap.append(button, status);
+});