diff --git a/src/__tests__/plugins/pluginsAdmin.test.tsx b/src/__tests__/plugins/pluginsAdmin.test.tsx
index e38ad9933..0a1f4905d 100644
--- a/src/__tests__/plugins/pluginsAdmin.test.tsx
+++ b/src/__tests__/plugins/pluginsAdmin.test.tsx
@@ -10,6 +10,7 @@ import type { CmsCurrentUser } from '@core/persistence'
import { makeSite } from '../fixtures'
const originalFetch = globalThis.fetch
+const originalWindowOpen = window.open
const mapManifest = {
id: 'local.map',
@@ -152,10 +153,43 @@ beforeEach(() => {
afterEach(() => {
globalThis.fetch = originalFetch
+ window.open = originalWindowOpen
cleanup()
})
describe('PluginsPage', () => {
+ it('opens the community plugin directory from the empty state', async () => {
+ const opened: unknown[][] = []
+ window.open = ((...args: unknown[]) => {
+ opened.push(args)
+ return null
+ }) as typeof window.open
+ globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
+ const url = String(input)
+ if (url === '/admin/api/cms/plugins' && init?.method === 'GET') {
+ return json({ plugins: [], adminPages: [] })
+ }
+ const ambient = ambientFetchFallback(url)
+ if (ambient) return ambient
+ return json({ error: `Unhandled ${url}` }, 500)
+ }
+
+ render(
+
+
+ ,
+ )
+
+ expect(await screen.findByText('No plugins installed yet.')).toBeDefined()
+ fireEvent.click(screen.getByRole('button', { name: 'Find Community Plugins' }))
+
+ expect(opened).toEqual([[
+ 'https://github.com/topics/instatic-plugin',
+ '_blank',
+ 'noopener,noreferrer',
+ ]])
+ })
+
it('lists active plugins and can disable or remove them', async () => {
const calls: Array<{ input: RequestInfo | URL; init?: RequestInit }> = []
globalThis.fetch = async (input: RequestInfo | URL, init?: RequestInit) => {
diff --git a/src/admin/pages/plugins/PluginsPage.module.css b/src/admin/pages/plugins/PluginsPage.module.css
index f1444b2db..c7db281b1 100644
--- a/src/admin/pages/plugins/PluginsPage.module.css
+++ b/src/admin/pages/plugins/PluginsPage.module.css
@@ -17,8 +17,7 @@
white-space: nowrap;
}
-.error,
-.emptyState {
+.error {
margin: 0;
font-size: var(--text-s);
line-height: 1.5;
@@ -56,13 +55,33 @@
}
.emptyState {
+ display: grid;
+ justify-items: center;
+ gap: var(--space-m);
padding: var(--space-6xl);
border: 1px dashed var(--overlay-10);
- border-radius: 8px;
+ border-radius: var(--editor-radius);
color: var(--text-subtle);
text-align: center;
}
+.emptyState h2,
+.emptyState p {
+ margin: 0;
+}
+
+.emptyState h2 {
+ color: var(--text);
+ font-size: var(--text-l);
+ line-height: 1.35;
+}
+
+.emptyState p {
+ max-width: 560px;
+ font-size: var(--text-s);
+ line-height: 1.5;
+}
+
.pluginsList {
display: grid;
gap: var(--space-px);
diff --git a/src/admin/pages/plugins/PluginsPage.tsx b/src/admin/pages/plugins/PluginsPage.tsx
index 85f38d56a..e133ad02a 100644
--- a/src/admin/pages/plugins/PluginsPage.tsx
+++ b/src/admin/pages/plugins/PluginsPage.tsx
@@ -1,4 +1,5 @@
import { Button } from '@ui/components/Button'
+import { ExternalLinkSolidIcon } from 'pixel-art-icons/icons/external-link-solid'
import { UploadIcon } from 'pixel-art-icons/icons/upload'
import { AdminPageLayout } from '@admin/layouts/AdminPageLayout'
import { PluginCard } from './components/PluginCard/PluginCard'
@@ -21,6 +22,11 @@ import styles from './PluginsPage.module.css'
// (e.g. host plugins + Analytics). PluginCard's `loading` prop owns
// the actual skeleton markup — page-level code only decides count.
const SKELETON_CARD_COUNT = 3
+const COMMUNITY_PLUGIN_DIRECTORY_URL = 'https://github.com/topics/instatic-plugin'
+
+function openCommunityPluginDirectory(): void {
+ window.open(COMMUNITY_PLUGIN_DIRECTORY_URL, '_blank', 'noopener,noreferrer')
+}
export function PluginsPage() {
const currentUser = useAuthenticatedAdminUser()
@@ -48,28 +54,40 @@ export function PluginsPage() {
workspace="plugins"
title="Plugins"
titleId="plugins-title"
- description="Install admin extensions and control what they add to the CMS."
- actions={canInstall ? (
+ description="Discover community extensions, install packages, and control what they add to the CMS."
+ actions={(
<>
fileInputRef.current?.click()}
+ onClick={openCommunityPluginDirectory}
>
-
- {uploading ? 'Uploading' : 'Upload Plugin'}
+
+ Browse Community Plugins
- void vm.handleUpload(event)}
- />
+ {canInstall && (
+ <>
+ fileInputRef.current?.click()}
+ >
+
+ {uploading ? 'Uploading' : 'Upload Plugin'}
+
+ void vm.handleUpload(event)}
+ />
+ >
+ )}
>
- ) : null}
+ )}
>
{error && (
@@ -144,7 +162,22 @@ export function PluginsPage() {
))
) : payload.plugins.length === 0 ? (
-
No plugins installed yet.
+
+
No plugins installed yet.
+
+ Browse community plugins on GitHub, download a release ZIP,
+ then upload it here. Review the source and requested permissions
+ before installing any community package.
+
+
+
+ Find Community Plugins
+
+
) : (
payload.plugins.map((plugin) => (