diff --git a/.github/ISSUE_TEMPLATE/add-plugin.yml b/.github/ISSUE_TEMPLATE/add-plugin.yml new file mode 100644 index 0000000..62a2e03 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/add-plugin.yml @@ -0,0 +1,122 @@ +name: Add a Plugin +description: Submit a new plugin to the Docusaurus Community Plugin Directory. +title: "[Plugin] " +labels: + - plugin-submission +body: + - type: markdown + attributes: + value: | + Fill in the fields below. A bot will generate your YAML file and open a draft PR automatically. + Review the [contributing guide](https://docusaurus.community/contributing/plugins) before submitting. + + - type: input + id: name + attributes: + label: Plugin name + description: Display name shown in the directory. + placeholder: "My Awesome Plugin" + validations: + required: true + + - type: input + id: id + attributes: + label: Plugin ID + description: "`author.plugin-short-name` — must be unique. Lowercase letters, numbers, and hyphens only." + placeholder: "yourname.plugin-short-name" + validations: + required: true + + - type: textarea + id: description + attributes: + label: Description + description: One sentence describing what the plugin does. + placeholder: "A plugin that does X." + validations: + required: true + + - type: input + id: website + attributes: + label: Website / Repository URL + description: Public website or repository URL. + placeholder: "https://github.com/yourname/your-plugin" + validations: + required: true + + - type: input + id: source + attributes: + label: Source code URL + description: Source code URL. Required for open-source plugins — leave blank for closed-source. + placeholder: "https://github.com/yourname/your-plugin" + + - type: input + id: author + attributes: + label: Author + description: Your GitHub username or organisation name. + placeholder: "yourname" + + - type: input + id: preview + attributes: + label: Preview image URL + description: URL to a preview image. Leave blank to auto-generate. + placeholder: "https://..." + + - type: dropdown + id: status + attributes: + label: Status + description: Current maintenance status of the plugin. + options: + - maintained + - unmaintained + - unknown + validations: + required: true + + - type: input + id: minimumVersion + attributes: + label: Minimum Docusaurus version + description: e.g. `3.0.0`. Leave blank if not applicable. + placeholder: "3.0.0" + + - type: checkboxes + id: tags + attributes: + label: Tags + description: Select all that apply. + options: + - label: search + - label: api + - label: utility + - label: content + - label: theme + - label: markdown + - label: analytics + - label: integration + - label: seo + - label: editing + - label: docusaurus + + - type: textarea + id: npmPackages + attributes: + label: npm package name(s) + description: One package name per line. + placeholder: "your-npm-package-name" + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: The plugin works with a current or recent stable release of Docusaurus. + required: true + - label: I have read the [contributing guide](https://docusaurus.community/contributing/plugins). + required: true diff --git a/.github/workflows/create-plugin-pr.yml b/.github/workflows/create-plugin-pr.yml new file mode 100644 index 0000000..b333f6d --- /dev/null +++ b/.github/workflows/create-plugin-pr.yml @@ -0,0 +1,197 @@ +name: Create Plugin PR from Issue + +on: + issues: + types: [opened] + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + create-pr: + if: contains(github.event.issue.labels.*.name, 'plugin-submission') + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v7 + with: + script: | + const body = context.payload.issue.body ?? ''; + const issueNumber = context.payload.issue.number; + const issueUrl = context.payload.issue.html_url; + + function parseField(body, heading) { + const escaped = heading.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(`### ${escaped}\\r?\\n\\r?\\n([\\s\\S]*?)(?=\\r?\\n### |$)`); + const match = body.match(regex); + if (!match) return null; + const val = match[1].trim(); + return val === '_No response_' || val === '' ? null : val; + } + + function parseCheckboxes(body, heading) { + const section = parseField(body, heading); + if (!section) return []; + return section.split('\n') + .filter(line => /^- \[x\]/i.test(line.trim())) + .map(line => line.replace(/^- \[x\] /i, '').trim()); + } + + const name = parseField(body, 'Plugin name'); + const id = parseField(body, 'Plugin ID'); + const description = parseField(body, 'Description'); + const website = parseField(body, 'Website / Repository URL'); + const source = parseField(body, 'Source code URL'); + const author = parseField(body, 'Author'); + const preview = parseField(body, 'Preview image URL'); + const status = parseField(body, 'Status'); + const minimumVersion = parseField(body, 'Minimum Docusaurus version'); + const npmPackagesRaw = parseField(body, 'npm package name(s)'); + const tags = parseCheckboxes(body, 'Tags'); + + const errors = []; + if (!name) errors.push('Plugin name is required'); + if (!id) errors.push('Plugin ID is required'); + if (id && !/^[a-z0-9-]+\.[a-z0-9-]+$/.test(id)) errors.push('Plugin ID must be in `author.plugin-name` format using lowercase letters, numbers, and hyphens only'); + if (!description) errors.push('Description is required'); + if (!website) errors.push('Website / Repository URL is required'); + if (!status) errors.push('Status is required'); + + if (errors.length > 0) { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: [ + 'Thank you for your plugin submission! Unfortunately some required fields are missing or invalid:', + '', + ...errors.map(e => `- ${e}`), + '', + 'Please close this issue and open a new one with all fields correctly filled in.', + ].join('\n'), + }); + return; + } + + const npmPackages = npmPackagesRaw + ? npmPackagesRaw.split('\n').map(l => l.trim()).filter(Boolean) + : []; + + const filename = `${id}.yaml`; + const branch = `plugin/${id}`; + + // Check if the file already exists + try { + await github.rest.repos.getContent({ + owner: context.repo.owner, + repo: context.repo.repo, + path: `data/plugins/${filename}`, + }); + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: `A plugin with ID \`${id}\` already exists in the directory. Please choose a unique ID, then close this issue and open a new one.`, + }); + return; + } catch (e) { + // File doesn't exist — expected + } + + // Generate YAML + const yamlLines = [ + '# yaml-language-server: $schema=https://docusaurus.community/schema/plugin/1.0.0.json', + `id: ${id}`, + `name: "${name.replace(/"/g, '\\"')}"`, + `description: "${description.replace(/"/g, '\\"')}"`, + `preview: ${preview || 'null'}`, + `website: ${website}`, + `source: ${source || 'null'}`, + `author: ${author || 'null'}`, + ]; + + if (tags.length > 0) { + yamlLines.push('tags:'); + tags.forEach(t => yamlLines.push(` - ${t}`)); + } else { + yamlLines.push('tags: []'); + } + + yamlLines.push(`minimumVersion: ${minimumVersion || 'null'}`); + yamlLines.push(`status: ${status}`); + + if (npmPackages.length > 0) { + yamlLines.push('npmPackages:'); + npmPackages.forEach(p => yamlLines.push(` - ${p}`)); + } else { + yamlLines.push('npmPackages: []'); + } + + const yaml = yamlLines.join('\n') + '\n'; + + // Get main branch SHA + const mainRef = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'heads/main', + }); + const baseSha = mainRef.data.object.sha; + + // Create branch, falling back to a unique name if it already exists + let actualBranch = branch; + try { + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/heads/${branch}`, + sha: baseSha, + }); + } catch (e) { + actualBranch = `${branch}-${issueNumber}`; + await github.rest.git.createRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `refs/heads/${actualBranch}`, + sha: baseSha, + }); + } + + // Commit the YAML file + await github.rest.repos.createOrUpdateFileContents({ + owner: context.repo.owner, + repo: context.repo.repo, + path: `data/plugins/${filename}`, + message: `chore: add plugin ${id} from issue #${issueNumber}`, + content: Buffer.from(yaml).toString('base64'), + branch: actualBranch, + }); + + // Open a draft PR + const pr = await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: `Add plugin: ${name}`, + head: actualBranch, + base: 'main', + body: [ + `Adds \`${id}\` to the plugin directory.`, + '', + `Generated automatically from issue ${issueUrl}.`, + '', + `Closes #${issueNumber}`, + ].join('\n'), + draft: true, + }); + + // Comment on the issue with the PR link + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issueNumber, + body: [ + `Thank you for your plugin submission! A draft PR has been created: ${pr.data.html_url}`, + '', + 'A maintainer will review it shortly. Please check the PR for any CI validation errors.', + ].join('\n'), + }); diff --git a/.yarn/install-state.gz b/.yarn/install-state.gz index 07e77d1..faa48b6 100644 Binary files a/.yarn/install-state.gz and b/.yarn/install-state.gz differ diff --git a/contributing/plugins.mdx b/contributing/plugins.mdx index 34517de..a0f1400 100644 --- a/contributing/plugins.mdx +++ b/contributing/plugins.mdx @@ -8,8 +8,9 @@ The plugin directory is powered by [`@homotechsual/docusaurus-plugin-showcase`]( ## Before you submit -- The plugin must be **open source** with a publicly accessible source repository. -- The plugin must work with a current or recent stable release of Docusaurus. +- If the plugin is **open source** the source repository must be publicly accessible and present in the `source` field of your YAML file. +- If the plugin is **closed source** the source repository must not be present in the `source` field of your YAML file. +- The plugin must work with a current or recent stable release of Docusaurus to have `status` set to `maintained`. If the plugin is no longer maintained, set `status` to `unmaintained`. If you are unsure, set `status` to `unknown`. - One entry per npm package (or logical plugin unit). ## Create your YAML file @@ -55,6 +56,12 @@ npmPackages: ## Submit a pull request +### Option A — automated (recommended) + +Open a [plugin submission issue](https://github.com/DocusaurusCommunity/website/issues/new?template=add-plugin.yml) and fill in the form. A bot will generate your YAML file, open a draft PR, and comment on the issue with a link. You do not need to fork the repository. + +### Option B — manual + 1. Fork [`DocusaurusCommunity/website`](https://github.com/DocusaurusCommunity/website) on GitHub. 2. Add your YAML file to `data/plugins/`. 3. Open a pull request against the `main` branch. diff --git a/docs/component-library/new/Card/index.mdx b/docs/component-library/new/Card/index.mdx index 4025133..db13e04 100644 --- a/docs/component-library/new/Card/index.mdx +++ b/docs/component-library/new/Card/index.mdx @@ -635,8 +635,10 @@ For convenience these cards are displayed using some wrapper div elements like a