Skip to content

Commit 1f1fafd

Browse files
chore: Static URL Support for Applications and Pages (#41312)
## 🎯 Overview This PR implements **Static URL** functionality for Appsmith applications and pages, allowing users to create clean, URLs without auto-generated IDs. This feature is gated behind the `release_static_url_enabled` feature flag. ## 🚀 Key Features ### 1. **Application-Level Static URLs** - **Enable/Disable Static URLs**: Toggle static URLs on/off at the application level - **Custom Application Slug**: Set a unique, human-readable slug for the entire application (e.g., `my-app` instead of `untitled-application-5`) - **Real-time Validation**: Client-side and server-side validation for slug format (lowercase letters, numbers, hyphens only) - **Availability Check**: Real-time checking if a slug is available or already taken - **Slug Suggestions**: Auto-generate slug suggestions based on application name ### 2. **Page-Level Static URLs** - **Custom Page Slugs**: Set unique slugs for individual pages (e.g., `dashboard` instead of `page1-68fb0a086001f8101c93e34q`) - **Per-Page Configuration**: Each page can have its own static slug - **Validation & Uniqueness**: Real-time validation and uniqueness checking for page slugs ### 3. **URL Format Changes** **Before (Legacy Format):** ``` /app/application-slug/page-name-<pageId> ``` **After (Static URL Format):** ``` /app/app-slug/page-slug ``` ## 🏗️ Technical Implementation ### **New Routes** - `BUILDER_PATH_STATIC`: `/app/:staticApplicationSlug/:staticPageSlug/edit` - `VIEWER_PATH_STATIC`: `/app/:staticApplicationSlug/:staticPageSlug` ### **New Redux Actions** - `ENABLE_STATIC_URL` / `DISABLE_STATIC_URL`: Toggle static URL feature - `PERSIST_APP_SLUG` / `PERSIST_PAGE_SLUG`: Save application/page slugs - `VALIDATE_APP_SLUG` / `VALIDATE_PAGE_SLUG`: Validate slug availability - `FETCH_APP_SLUG_SUGGESTION`: Get auto-generated suggestions ### **New API Endpoints** #### Application APIs - `POST /api/v1/applications/:applicationId/static-url` - Enable static URL with initial slug - `PUT /api/v1/applications/:applicationId/static-url` - Update application slug - `DELETE /api/v1/applications/:applicationId/static-url` - Disable static URL - `GET /api/v1/applications/:applicationId/static-url/:uniqueSlug` - Validate application slug availability - `GET /api/v1/applications/:applicationId/static-url/suggest-app-slug` - Get slug suggestion #### Page APIs - `PATCH /api/v1/pages/static-url` - Update page slug - `GET /api/v1/pages/:pageId/static-url/verify/:uniqueSlug` - Validate page slug availability ### **URL Assembly Enhancements** - Extended `URLAssembly` to support static URL generation - Added `URL_TYPE.STATIC` for static URL routing - Static URLs only apply in **published/viewer mode** (edit mode uses regular URLs) - Automatic fallback: If `staticPageSlug` is not set, falls back to regular `pageSlug` ## 🎨 UI Components ### **General Settings (Application Level)** - **Static URL Toggle**: Enable/disable static URLs for the application - **Application Slug Input**: - Real-time validation with error messages - Loading states during validation - Success/error indicators - URL preview showing the final URL - **Apply/Cancel Buttons**: Persist or discard changes - **Confirmation Modal**: Shows before/after URLs when changing or disabling ### **Page Settings (Page Level)** - **Page Slug Input**: Similar to application slug with validation - **URL Preview**: Shows the complete page URL - **Apply/Cancel Actions**: Per-page slug management ### **StaticURLConfirmationModal** - Displays "From" and "To" URLs - Warning message about breaking existing links - Different modes: "Change" vs "Disable" - Bold formatting for app/page slugs in URLs ## 🔄 State Management ### **Application Reducer** - `uniqueSlug`: Stores the application's static slug - `staticUrlEnabled`: Boolean flag for static URL status ### **Page List Reducer** - `uniqueSlug`: Stores each page's static slug - Updates only the specific page when slug is persisted ### **Applications UI Reducer** - `isValidatingAppSlug`: Validation in progress - `isApplicationSlugValid`: Validation result - `isFetchingAppSlugSuggestion`: Fetching suggestion - `appSlugSuggestion`: Auto-generated suggestion - `isPersistingAppSlug`: Save in progress ## 🛡️ Validation Rules **Application/Page Slug Requirements:** - Only lowercase letters (a-z) - Numbers (0-9) - Hyphens (-) - No spaces or special characters - Must be unique across the workspace - Cannot be empty ## 🔀 Navigation & Routing ### **Route Matching Updates** - Extended `matchBuilderPath` to include `BUILDER_PATH_STATIC` - Extended `matchViewerPath` to include `VIEWER_PATH_STATIC` - Updated route params to include optional `staticApplicationSlug` and `staticPageSlug` ### **Page Navigation** - New hook: `useStaticUrlGeneration` for generating static URLs in navigation - Updated `useNavigateToAnotherPage` to support static URLs - Menu items and navigation components updated to use static URLs when available ## 📦 Key Files Changed **Core Logic:** - `URLAssembly.ts`: URL generation logic with static URL support - `ApplicationSagas.tsx`: Sagas for enable/disable/persist operations - `PageSagas.tsx`: Page-level slug persistence - `appRoutes.ts`: New static URL routes **UI Components:** - `GeneralSettings.tsx`: Application-level static URL configuration - `PageSettings.tsx`: Page-level static URL configuration - `StaticURLConfirmationModal.tsx`: Confirmation dialog (new) - `UrlPreview.tsx`: URL preview component (new) **State Management:** - `applicationsReducer.tsx`: Application UI state - `appReducer.ts`: Application entity state - `pageListReducer.tsx`: Page list state ## ⚙️ Feature Flag - `release_static_url_enabled`: Controls visibility of static URL features ## Automation /ok-to-test tags="@tag.All" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/18936467359> > Commit: a74b4ac > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=18936467359&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.All` > Spec: > <hr>Thu, 30 Oct 2025 10:56:15 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Static URL support for apps and pages: enable/disable static URLs, edit app/page slugs, validate and persist slugs, and receive slug suggestions. * URL preview with click-to-copy and a confirmation modal when changing or disabling app slugs. * **Bug Fixes / Improvements** * Routing, editor and viewer flows updated for slug-based paths. * Base page resolution and editor links now derive from application state for more reliable navigation and loading. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Pedro Santos Rodrigues <pedro@appsmith.com>
1 parent a30daa6 commit 1f1fafd

File tree

61 files changed

+2519
-233
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

61 files changed

+2519
-233
lines changed

app/client/src/PluginActionEditor/components/PluginActionResponse/components/BindDataButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
import { useDispatch, useSelector } from "react-redux";
1818
import {
1919
getCurrentApplicationId,
20+
getCurrentBasePageId,
2021
getPageList,
2122
getPagePermissions,
2223
} from "selectors/editorSelectors";
@@ -247,11 +248,11 @@ function BindDataButton(props: BindDataButtonProps) {
247248
const pagePermissions = useSelector(getPagePermissions);
248249

249250
const params = useParams<{
250-
basePageId: string;
251251
baseApiId?: string;
252252
baseQueryId?: string;
253253
moduleInstanceId?: string;
254254
}>();
255+
const currentBasePageId = useSelector(getCurrentBasePageId);
255256

256257
const isFeatureEnabled = useFeatureFlag(FEATURE_FLAG.license_gac_enabled);
257258

@@ -346,7 +347,7 @@ function BindDataButton(props: BindDataButtonProps) {
346347
params.baseQueryId ||
347348
params.moduleInstanceId) as string,
348349
applicationId: applicationId as string,
349-
basePageId: params.basePageId,
350+
basePageId: currentBasePageId,
350351
}),
351352
);
352353

app/client/src/actions/initActions.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ export interface InitEditorActionPayload {
1414
branch?: string;
1515
mode: APP_MODE;
1616
shouldInitialiseUserDetails?: boolean;
17+
staticApplicationSlug?: string;
18+
staticPageSlug?: string;
1719
}
1820

1921
export const initEditorAction = (
@@ -26,9 +28,11 @@ export const initEditorAction = (
2628
export interface InitAppViewerPayload {
2729
branch: string;
2830
baseApplicationId?: string;
29-
basePageId: string;
31+
basePageId?: string;
3032
mode: APP_MODE;
3133
shouldInitialiseUserDetails?: boolean;
34+
staticApplicationSlug?: string;
35+
staticPageSlug?: string;
3236
}
3337

3438
export const initAppViewerAction = ({
@@ -37,6 +41,8 @@ export const initAppViewerAction = ({
3741
branch,
3842
mode,
3943
shouldInitialiseUserDetails,
44+
staticApplicationSlug,
45+
staticPageSlug,
4046
}: InitAppViewerPayload) => ({
4147
type: ReduxActionTypes.INITIALIZE_PAGE_VIEWER,
4248
payload: {
@@ -45,6 +51,8 @@ export const initAppViewerAction = ({
4551
basePageId,
4652
mode,
4753
shouldInitialiseUserDetails,
54+
staticApplicationSlug,
55+
staticPageSlug,
4856
},
4957
});
5058

app/client/src/actions/pageActions.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -705,3 +705,23 @@ export const navigateToAnotherPage = (
705705
type: ReduxActionTypes.NAVIGATE_TO_ANOTHER_PAGE,
706706
payload,
707707
});
708+
709+
export const persistPageSlug = (pageId: string, slug: string) => {
710+
return {
711+
type: ReduxActionTypes.PERSIST_PAGE_SLUG,
712+
payload: {
713+
pageId,
714+
slug,
715+
},
716+
};
717+
};
718+
719+
export const validatePageSlug = (pageId: string, slug: string) => {
720+
return {
721+
type: ReduxActionTypes.VALIDATE_PAGE_SLUG,
722+
payload: {
723+
pageId,
724+
slug,
725+
},
726+
};
727+
};

app/client/src/api/PageApi.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ export interface UpdatePageRequest {
8989
name?: string;
9090
isHidden?: boolean;
9191
customSlug?: string;
92+
uniqueSlug?: string;
9293
}
9394

9495
export interface UpdatePageResponse {
@@ -97,6 +98,7 @@ export interface UpdatePageResponse {
9798
name: string;
9899
slug: string;
99100
customSlug?: string;
101+
uniqueSlug?: string;
100102
applicationId: string;
101103
layouts: Array<PageLayout>;
102104
isHidden: boolean;
@@ -300,6 +302,20 @@ class PageApi extends Api {
300302
): Promise<AxiosPromise<FetchApplicationResponse>> {
301303
return Api.get(PageApi.url, params);
302304
}
305+
306+
static async persistPageSlug(request: {
307+
branchedPageId: string;
308+
uniquePageSlug: string;
309+
}): Promise<AxiosPromise<ApiResponse>> {
310+
return Api.patch(`${PageApi.url}/static-url`, request);
311+
}
312+
313+
static async validatePageSlug(
314+
pageId: string,
315+
uniqueSlug: string,
316+
): Promise<AxiosPromise<ApiResponse>> {
317+
return Api.get(`${PageApi.url}/${pageId}/static-url/verify/${uniqueSlug}`);
318+
}
303319
}
304320

305321
export default PageApi;

app/client/src/api/services/ConsolidatedPageLoadApi/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,6 @@ export interface ConsolidatedApiParams {
22
applicationId?: string;
33
defaultPageId?: string;
44
branchName?: string;
5+
staticApplicationSlug?: string;
6+
staticPageSlug?: string;
57
}

app/client/src/ce/AppRouter.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
BUILDER_PATCH_PATH,
1515
BUILDER_PATH,
1616
BUILDER_PATH_DEPRECATED,
17+
BUILDER_PATH_STATIC,
1718
CUSTOM_WIDGETS_DEPRECATED_EDITOR_ID_PATH,
1819
CUSTOM_WIDGETS_EDITOR_ID_PATH,
1920
CUSTOM_WIDGETS_EDITOR_ID_PATH_CUSTOM,
@@ -27,6 +28,7 @@ import {
2728
VIEWER_PATCH_PATH,
2829
VIEWER_PATH,
2930
VIEWER_PATH_DEPRECATED,
31+
VIEWER_PATH_STATIC,
3032
WORKSPACE_URL,
3133
} from "constants/routes";
3234
import WorkspaceLoader from "pages/workspace/loader";
@@ -135,6 +137,9 @@ export function Routes() {
135137
{/*
136138
* End Note: When making changes to the order of the paths above
137139
*/}
140+
{/* Static URL routes that accept any page slug - must be after more specific routes */}
141+
<SentryRoute component={AppIDE} path={BUILDER_PATH_STATIC} />
142+
<SentryRoute component={AppViewerLoader} path={VIEWER_PATH_STATIC} />
138143
<Redirect from={BUILDER_PATCH_PATH} to={BUILDER_PATH} />
139144
<Redirect from={VIEWER_PATCH_PATH} to={VIEWER_PATH} />
140145
<SentryRoute component={PageNotFound} />
@@ -175,14 +180,14 @@ export default function AppRouter() {
175180
return (
176181
<Router history={history}>
177182
<Suspense fallback={loadingIndicator}>
178-
<RouteChangeListener />
179183
{safeCrash && safeCrashCode ? (
180184
<>
181185
<ErrorPageHeader />
182186
<ErrorPage code={safeCrashCode} />
183187
</>
184188
) : (
185189
<>
190+
<RouteChangeListener />
186191
<Walkthrough>
187192
<AppHeader />
188193
<Routes />

app/client/src/ce/IDE/constants/routes.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
BUILDER_CUSTOM_PATH,
66
BUILDER_PATH,
77
BUILDER_PATH_DEPRECATED,
8+
BUILDER_PATH_STATIC,
89
DATA_SOURCES_EDITOR_ID_PATH,
910
ENTITY_PATH,
1011
INTEGRATION_EDITOR_PATH,
@@ -40,6 +41,11 @@ export const EntityPaths: string[] = [
4041
];
4142
export const IDEBasePaths: Readonly<Record<IDEType, string[]>> = {
4243
[IDE_TYPE.None]: [],
43-
[IDE_TYPE.App]: [BUILDER_PATH, BUILDER_PATH_DEPRECATED, BUILDER_CUSTOM_PATH],
44+
[IDE_TYPE.App]: [
45+
BUILDER_PATH,
46+
BUILDER_PATH_DEPRECATED,
47+
BUILDER_CUSTOM_PATH,
48+
BUILDER_PATH_STATIC,
49+
],
4450
[IDE_TYPE.UIPackage]: [],
4551
};

app/client/src/ce/actions/applicationActions.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,34 @@ export const updateApplication = (
7979
};
8080
};
8181

82+
export const persistAppSlug = (slug: string, onSuccess?: () => void) => {
83+
return {
84+
type: ReduxActionTypes.PERSIST_APP_SLUG,
85+
payload: {
86+
slug,
87+
onSuccess,
88+
},
89+
};
90+
};
91+
92+
export const validateAppSlug = (slug: string) => {
93+
return {
94+
type: ReduxActionTypes.VALIDATE_APP_SLUG,
95+
payload: {
96+
slug,
97+
},
98+
};
99+
};
100+
101+
export const fetchAppSlugSuggestion = (applicationId: string) => {
102+
return {
103+
type: ReduxActionTypes.FETCH_APP_SLUG_SUGGESTION,
104+
payload: {
105+
applicationId,
106+
},
107+
};
108+
};
109+
82110
export const updateCurrentApplicationIcon = (icon: IconNames) => {
83111
return {
84112
type: ReduxActionTypes.CURRENT_APPLICATION_ICON_UPDATE,
@@ -290,3 +318,28 @@ export const setIsAppSidebarPinned = (payload: boolean) => ({
290318
export const fetchAllPackages = () => {
291319
return {};
292320
};
321+
322+
export const enableStaticUrl = (slug: string, onSuccess?: () => void) => {
323+
return {
324+
type: ReduxActionTypes.ENABLE_STATIC_URL,
325+
payload: {
326+
slug,
327+
onSuccess,
328+
},
329+
};
330+
};
331+
332+
export const disableStaticUrl = (onSuccess?: () => void) => {
333+
return {
334+
type: ReduxActionTypes.DISABLE_STATIC_URL,
335+
payload: {
336+
onSuccess,
337+
},
338+
};
339+
};
340+
341+
export const resetAppSlugValidation = () => {
342+
return {
343+
type: ReduxActionTypes.RESET_APP_SLUG_VALIDATION,
344+
};
345+
};

app/client/src/ce/api/ApplicationApi.tsx

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export interface ApplicationPagePayload {
3535
slug: string;
3636
isHidden?: boolean;
3737
customSlug?: string;
38+
uniqueSlug?: string;
3839
userPermissions?: string[];
3940
}
4041

@@ -517,6 +518,52 @@ export class ApplicationApi extends Api {
517518
> {
518519
return Api.post(`${ApplicationApi.baseURL}/import/partial/block`, request);
519520
}
521+
522+
static async persistAppSlug(
523+
applicationId: string,
524+
request: {
525+
branchedApplicationId: string;
526+
uniqueApplicationSlug: string;
527+
},
528+
): Promise<AxiosPromise<ApiResponse>> {
529+
return Api.patch(
530+
`${ApplicationApi.baseURL}/${applicationId}/static-url`,
531+
request,
532+
);
533+
}
534+
535+
static async validateAppSlug(
536+
applicationId: string,
537+
uniqueSlug: string,
538+
): Promise<AxiosPromise<ApiResponse>> {
539+
return Api.get(
540+
`${ApplicationApi.baseURL}/${applicationId}/static-url/${uniqueSlug}`,
541+
);
542+
}
543+
544+
static async enableStaticUrl(
545+
applicationId: string,
546+
request: { uniqueApplicationSlug: string },
547+
): Promise<AxiosPromise<ApiResponse>> {
548+
return Api.post(
549+
`${ApplicationApi.baseURL}/${applicationId}/static-url`,
550+
request,
551+
);
552+
}
553+
554+
static async disableStaticUrl(
555+
applicationId: string,
556+
): Promise<AxiosPromise<ApiResponse>> {
557+
return Api.delete(`${ApplicationApi.baseURL}/${applicationId}/static-url`);
558+
}
559+
560+
static async fetchAppSlugSuggestion(
561+
applicationId: string,
562+
): Promise<AxiosPromise<ApiResponse>> {
563+
return Api.get(
564+
`${ApplicationApi.baseURL}/${applicationId}/static-url/suggest-app-slug`,
565+
);
566+
}
520567
}
521568

522569
export default ApplicationApi;

app/client/src/ce/constants/ReduxActionConstants.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -636,9 +636,28 @@ const PageActionErrorTypes = {
636636
};
637637

638638
const ApplicationActionTypes = {
639+
ENABLE_STATIC_URL: "ENABLE_STATIC_URL",
640+
ENABLE_STATIC_URL_SUCCESS: "ENABLE_STATIC_URL_SUCCESS",
641+
DISABLE_STATIC_URL: "DISABLE_STATIC_URL",
642+
DISABLE_STATIC_URL_SUCCESS: "DISABLE_STATIC_URL_SUCCESS",
639643
UPDATE_APPLICATION: "UPDATE_APPLICATION",
640644
UPDATE_APP_LAYOUT: "UPDATE_APP_LAYOUT",
641645
UPDATE_APPLICATION_SUCCESS: "UPDATE_APPLICATION_SUCCESS",
646+
PERSIST_APP_SLUG: "PERSIST_APP_SLUG",
647+
PERSIST_APP_SLUG_SUCCESS: "PERSIST_APP_SLUG_SUCCESS",
648+
PERSIST_APP_SLUG_ERROR: "PERSIST_APP_SLUG_ERROR",
649+
RESET_APP_SLUG_VALIDATION: "RESET_APP_SLUG_VALIDATION",
650+
VALIDATE_APP_SLUG: "VALIDATE_APP_SLUG",
651+
VALIDATE_APP_SLUG_SUCCESS: "VALIDATE_APP_SLUG_SUCCESS",
652+
VALIDATE_APP_SLUG_ERROR: "VALIDATE_APP_SLUG_ERROR",
653+
PERSIST_PAGE_SLUG: "PERSIST_PAGE_SLUG",
654+
PERSIST_PAGE_SLUG_SUCCESS: "PERSIST_PAGE_SLUG_SUCCESS",
655+
PERSIST_PAGE_SLUG_ERROR: "PERSIST_PAGE_SLUG_ERROR",
656+
VALIDATE_PAGE_SLUG: "VALIDATE_PAGE_SLUG",
657+
VALIDATE_PAGE_SLUG_SUCCESS: "VALIDATE_PAGE_SLUG_SUCCESS",
658+
VALIDATE_PAGE_SLUG_ERROR: "VALIDATE_PAGE_SLUG_ERROR",
659+
FETCH_APP_SLUG_SUGGESTION: "FETCH_APP_SLUG_SUGGESTION",
660+
FETCH_APP_SLUG_SUGGESTION_SUCCESS: "FETCH_APP_SLUG_SUGGESTION_SUCCESS",
642661
FETCH_APPLICATION_INIT: "FETCH_APPLICATION_INIT",
643662
FETCH_APPLICATION_SUCCESS: "FETCH_APPLICATION_SUCCESS",
644663
CREATE_APPLICATION_INIT: "CREATE_APPLICATION_INIT",
@@ -670,6 +689,9 @@ const ApplicationActionErrorTypes = {
670689
DELETE_APPLICATION_ERROR: "DELETE_APPLICATION_ERROR",
671690
SET_DEFAULT_APPLICATION_PAGE_ERROR: "SET_DEFAULT_APPLICATION_PAGE_ERROR",
672691
FORK_APPLICATION_ERROR: "FORK_APPLICATION_ERROR",
692+
FETCH_APP_SLUG_SUGGESTION_ERROR: "FETCH_APP_SLUG_SUGGESTION_ERROR",
693+
ENABLE_STATIC_URL_ERROR: "ENABLE_STATIC_URL_ERROR",
694+
DISABLE_STATIC_URL_ERROR: "DISABLE_STATIC_URL_ERROR",
673695
};
674696

675697
const IDEDebuggerActionTypes = {

0 commit comments

Comments
 (0)