diff --git a/.changeset/swift-stingrays-cough.md b/.changeset/swift-stingrays-cough.md
new file mode 100644
index 00000000000..755e801d138
--- /dev/null
+++ b/.changeset/swift-stingrays-cough.md
@@ -0,0 +1,5 @@
+---
+"@clerk/astro": minor
+---
+
+Add support for custom pages and links in the `` Astro component.
diff --git a/integration/templates/astro-node/src/pages/custom-pages.astro b/integration/templates/astro-node/src/pages/custom-pages.astro
deleted file mode 100644
index 21d598adc7b..00000000000
--- a/integration/templates/astro-node/src/pages/custom-pages.astro
+++ /dev/null
@@ -1,22 +0,0 @@
----
-import { UserProfile } from "@clerk/astro/components";
-import Layout from "../layouts/Layout.astro";
----
-
-
-
-
-
-
Icon
-
-
Custom Terms Page
-
This is the custom terms page
-
-
-
-
Icon
-
-
-
-
-
diff --git a/integration/templates/astro-node/src/pages/custom-pages/organization-profile.astro b/integration/templates/astro-node/src/pages/custom-pages/organization-profile.astro
new file mode 100644
index 00000000000..a00aeed5672
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/custom-pages/organization-profile.astro
@@ -0,0 +1,51 @@
+---
+import {
+ OrganizationProfile as OrganizationProfileAstro,
+ OrganizationSwitcher
+} from "@clerk/astro/components";
+import Layout from "../../layouts/Layout.astro";
+
+// Added a dedicatedPage query param to conditionally render the OrganizationProfile
+// as for some reason, the menu items in the OrganizationSwitcher
+// goes out of bounds in test environment.
+const dedicatedPage = Astro.url.searchParams.get('dedicatedPage') === 'true';
+---
+
+
+
+
+ {
+ !dedicatedPage && (
+
+
Icon
+
+
Custom Terms Page
+
This is the custom terms page
+
+
+
+
Icon
+
+
+ )
+ }
+
+ {
+ dedicatedPage && (
+
+
+
Icon
+
+
Custom Terms Page
+
This is the custom terms page
+
+
+
+
Icon
+
+
+
+ )
+ }
+
+
diff --git a/integration/templates/astro-node/src/pages/custom-pages/user-profile.astro b/integration/templates/astro-node/src/pages/custom-pages/user-profile.astro
new file mode 100644
index 00000000000..fd9da776934
--- /dev/null
+++ b/integration/templates/astro-node/src/pages/custom-pages/user-profile.astro
@@ -0,0 +1,22 @@
+---
+import { UserProfile as UserProfileAstro } from "@clerk/astro/components";
+import Layout from "../../layouts/Layout.astro";
+---
+
+
+
+
+
+
Icon
+
+
Custom Terms Page
+
This is the custom terms page
+
+
+
+
Icon
+
+
+
+
+
diff --git a/integration/tests/astro/components.test.ts b/integration/tests/astro/components.test.ts
index 62a697e71d9..8f9b3f9dfd3 100644
--- a/integration/tests/astro/components.test.ts
+++ b/integration/tests/astro/components.test.ts
@@ -164,7 +164,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
await u.po.expect.toBeSignedIn();
- await u.page.goToRelative('/custom-pages');
+ await u.page.goToRelative('/custom-pages/user-profile');
await u.po.userProfile.waitForMounted();
// Check if custom pages and links are visible
@@ -176,7 +176,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible();
// Check reordered default label. Security tab is now the last item.
- await u.page.locator('.cl-navbarButton').nth(3).click();
+ await u.page.locator('.cl-navbarButton').last().click();
await expect(u.page.getByRole('heading', { name: 'Security' })).toBeVisible();
// Click custom link and check navigation
@@ -252,7 +252,7 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await fakeAdmin.deleteIfExists();
});
- test('test updateClerkOptions by changing localization on the fly', async ({ page, context }) => {
+ test('updateClerkOptions by changing localization on the fly', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
await u.po.signIn.goTo();
await u.po.signIn.waitForMounted();
@@ -268,6 +268,70 @@ testAgainstRunningApps({ withPattern: ['astro.node.withCustomRoles'] })('basic f
await expect(u.page.getByText('pour continuer vers')).toBeVisible();
});
+ test('render organization profile with custom pages and links in dedicated page', async ({ page, context }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ await u.page.goToRelative('/custom-pages/organization-profile?dedicatedPage=true');
+ await u.po.organizationSwitcher.waitForMounted();
+ await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
+
+ // Check if custom pages and links are visible
+ await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible();
+ await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible();
+
+ // Navigate to custom page
+ await u.page.getByRole('button', { name: /Terms/i }).click();
+ await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible();
+
+ // Check reordered default label. General tab is now the last item.
+ await u.page.locator('.cl-navbarButton').last().click();
+ await expect(u.page.getByRole('heading', { name: 'General' })).toBeVisible();
+
+ // Click custom link and check navigation
+ await u.page.getByRole('button', { name: /Homepage/i }).click();
+ await u.page.waitForAppUrl('/');
+ });
+
+ test('render organization profile with custom pages and links inside organization switcher', async ({
+ page,
+ context,
+ }) => {
+ const u = createTestUtils({ app, page, context });
+ await u.page.goToRelative('/sign-in');
+ await u.po.signIn.waitForMounted();
+ await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeAdmin.email, password: fakeAdmin.password });
+ await u.po.expect.toBeSignedIn();
+
+ await u.page.goToRelative('/custom-pages/organization-profile?dedicatedPage=false');
+ await u.po.organizationSwitcher.waitForMounted();
+ await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
+
+ // Open organization profile inside organization switcher
+ await u.po.organizationSwitcher.toggleTrigger();
+ await u.page.waitForSelector('.cl-organizationSwitcherPopoverCard', { state: 'visible' });
+ await u.page.locator('.cl-button__manageOrganization').click();
+
+ // Check if custom pages and links are visible
+ await expect(u.page.getByRole('button', { name: /Terms/i })).toBeVisible();
+ await expect(u.page.getByRole('button', { name: /Homepage/i })).toBeVisible();
+
+ // Navigate to custom page
+ await u.page.getByRole('button', { name: /Terms/i }).click();
+ await expect(u.page.getByRole('heading', { name: 'Custom Terms Page' })).toBeVisible();
+
+ // Check reordered default label. Members tab is now the last item.
+ await u.page.locator('.cl-navbarButton').last().click();
+ await expect(u.page.getByRole('heading', { name: 'Members' })).toBeVisible();
+
+ // Click custom link and check navigation
+ await u.page.getByRole('button', { name: /Homepage/i }).click();
+ await u.page.waitForAppUrl('/');
+ });
+
// ---- react/protect
test('only admin react', async ({ page, context }) => {
const u = createTestUtils({ app, page, context });
diff --git a/packages/astro/src/astro-components/index.ts b/packages/astro/src/astro-components/index.ts
index dd6725c1642..bd41eafb34f 100644
--- a/packages/astro/src/astro-components/index.ts
+++ b/packages/astro/src/astro-components/index.ts
@@ -20,7 +20,7 @@ export { default as SignIn } from './interactive/SignIn.astro';
export { default as SignUp } from './interactive/SignUp.astro';
export { UserButton } from './interactive/UserButton';
export { UserProfile } from './interactive/UserProfile';
-export { default as OrganizationProfile } from './interactive/OrganizationProfile.astro';
-export { default as OrganizationSwitcher } from './interactive/OrganizationSwitcher.astro';
+export { OrganizationProfile } from './interactive/OrganizationProfile';
+export { OrganizationSwitcher } from './interactive/OrganizationSwitcher';
export { default as OrganizationList } from './interactive/OrganizationList.astro';
export { default as GoogleOneTap } from './interactive/GoogleOneTap.astro';
diff --git a/packages/astro/src/astro-components/interactive/CustomProfilePageRenderer.astro b/packages/astro/src/astro-components/interactive/CustomProfilePageRenderer.astro
new file mode 100644
index 00000000000..a35b39c0564
--- /dev/null
+++ b/packages/astro/src/astro-components/interactive/CustomProfilePageRenderer.astro
@@ -0,0 +1,76 @@
+---
+interface Props {
+ url: string
+ label: string
+ type: 'page' | 'link'
+ component: 'organization-profile' | 'user-profile' | 'organization-switcher'
+ reorderItemsLabels?: Readonly>
+}
+
+const { url, label, type, component, reorderItemsLabels = [] } = Astro.props
+
+let labelIcon = '';
+let content = ''
+
+if (Astro.slots.has('label-icon')) {
+ labelIcon = await Astro.slots.render('label-icon');
+}
+
+if (Astro.slots.has('default') && type === 'page') {
+ content = await Astro.slots.render('default');
+}
+---
+
+
diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile.astro
deleted file mode 100644
index b504c003180..00000000000
--- a/packages/astro/src/astro-components/interactive/OrganizationProfile.astro
+++ /dev/null
@@ -1,8 +0,0 @@
----
-import type { OrganizationProfileProps } from "@clerk/types";
-type Props = OrganizationProfileProps
-
-import InternalUIComponentRenderer from './InternalUIComponentRenderer.astro'
----
-
-
diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfile.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfile.astro
new file mode 100644
index 00000000000..759c9584ca9
--- /dev/null
+++ b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfile.astro
@@ -0,0 +1,10 @@
+---
+import type { OrganizationProfileProps, Without } from '@clerk/types'
+
+type Props = Without
+
+import InternalUIComponentRenderer from '../InternalUIComponentRenderer.astro'
+---
+
+
+
diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfileLink.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfileLink.astro
new file mode 100644
index 00000000000..3f1280adcff
--- /dev/null
+++ b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfileLink.astro
@@ -0,0 +1,14 @@
+---
+import CustomProfilePageRenderer from '../CustomProfilePageRenderer.astro'
+
+interface Props {
+ url: string
+ label: string
+}
+
+const { url, label } = Astro.props
+---
+
+
+
+
diff --git a/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfilePage.astro b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfilePage.astro
new file mode 100644
index 00000000000..8b919a6aa35
--- /dev/null
+++ b/packages/astro/src/astro-components/interactive/OrganizationProfile/OrganizationProfilePage.astro
@@ -0,0 +1,24 @@
+---
+import CustomProfilePageRenderer from '../CustomProfilePageRenderer.astro'
+
+const reorderItemsLabels = ['general', 'members'] as const;
+type ReorderItemsLabels = typeof reorderItemsLabels[number];
+
+type Props