Skip to content

Commit a1c157d

Browse files
authored
Merge pull request #11579 from neinteractiveliterature/static-html-spa-routes
Move towards fully-static HTML page serving
2 parents 2359a61 + 05c16ef commit a1c157d

18 files changed

Lines changed: 131 additions & 34 deletions

.rubocop_todo.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,22 @@ Rails/RefuteMethods:
7575
# Offense count: 3
7676
# Configuration parameters: ForbiddenMethods, AllowedMethods.
7777
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
78+
Rails/HasAndBelongsToMany:
79+
Exclude:
80+
- 'app/models/page.rb'
81+
82+
Rails/HasManyOrHasOneDependent:
83+
Exclude:
84+
- 'app/models/page.rb'
85+
86+
Rails/HelperInstanceVariable:
87+
Exclude:
88+
- 'app/helpers/application_helper.rb'
89+
7890
Rails/SkipsModelValidations:
7991
Exclude:
8092
- 'app/controllers/application_controller.rb'
93+
- 'app/models/page.rb'
8194
- 'db/migrate/20251229184620_allow_null_redirect_uri_on_oauth_appliations.rb'
8295

8396
# Offense count: 7

CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ Intercode is a convention management system built with:
2626
- **TypeScript**: Run `yarn run tsc --noEmit` after making changes
2727
- **Ruby**: Run the relevant test suite before committing
2828

29+
## Deployment Model
30+
31+
Intercode is shipped as a Docker container with the frontend assets **pre-built at image build time**. Environment variables (including `ASSETS_HOST`, `SENTRY_FRONTEND_DSN`, `ROLLBAR_CLIENT_ACCESS_TOKEN`, etc.) are injected at **container runtime**, not at build time. This means:
32+
33+
- Do not use Vite `define`, `import.meta.env`, or any other build-time constant substitution for runtime env vars.
34+
- Runtime configuration must be delivered to the browser via a server-rendered response (e.g. the `GET /client_configuration` endpoint) or a server-rendered HTML attribute, never baked into the JS bundle.
35+
2936
## Testing Requirements
3037

3138
Whenever changing signup-related functionality (signup services, ranked choice, waitlists, etc.), always add or update tests in the relevant test file under `test/services/` or `test/models/`.

app/controllers/client_configuration_controller.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,10 @@ def show
1616
oidc_issuer_url: oidc_issuer_url,
1717
rails_default_active_storage_service_name: Rails.application.config.active_storage.service.to_s,
1818
rails_direct_uploads_url: rails_direct_uploads_url,
19-
recaptcha_site_key: Recaptcha.configuration.site_key
19+
recaptcha_site_key: Recaptcha.configuration.site_key,
20+
assets_host: ENV["ASSETS_HOST"].presence,
21+
sentry_frontend_dsn: ENV["SENTRY_FRONTEND_DSN"].presence,
22+
rollbar_client_access_token: ENV["ROLLBAR_CLIENT_ACCESS_TOKEN"].presence
2023
}
2124
end
2225
end

app/helpers/application_helper.rb

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,7 @@ def page_title
1818
end
1919

2020
def open_graph_description_for_page(page)
21-
return page.meta_description if page.meta_description.present?
22-
23-
Rails
24-
.cache
25-
.fetch(["open_graph_description", page], expires_in: 1.day) do
26-
strip_tags(cms_rendering_context.render_page_content(@page)).gsub(/\s+/, " ").strip.truncate(160)
27-
end
21+
page.meta_description.presence || page.cached_og_description
2822
end
2923

3024
def application_entry_path

app/javascript/@types/globals.d.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
interface Window {
22
intercodeAssetsHost?: string;
3-
sentryFrontendDSN?: string;
4-
rollbarClientAccessToken?: string;
53
__intercodeAssetURL: (filename: string) => string;
64
}

app/javascript/AppContexts.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export type ClientConfiguration = {
1313
rails_default_active_storage_service_name: string;
1414
rails_direct_uploads_url: string;
1515
recaptcha_site_key: string | null;
16+
assets_host: string | null;
17+
sentry_frontend_dsn: string | null;
18+
rollbar_client_access_token: string | null;
1619
};
1720

1821
export const authenticityTokensManagerContext = createContext<AuthenticityTokensManager>();

app/javascript/AppRoot.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Suspense, useMemo, useState, useEffect, useRef } from 'react';
2-
import { useLocation, useLoaderData, Outlet, useNavigation } from 'react-router';
2+
import { useLocation, useLoaderData, useRouteLoaderData, Outlet, useNavigation } from 'react-router';
33
import { Settings } from 'luxon';
44
import { PageLoadingIndicator } from '@neinteractiveliterature/litform';
55

@@ -12,6 +12,7 @@ import { LazyStripeContext } from './LazyStripe';
1212
import { Stripe } from '@stripe/stripe-js';
1313
import { reloadOnAppEntrypointHeadersMismatch } from './checkAppEntrypointHeadersMatch';
1414
import { initErrorReporting } from 'ErrorReporting';
15+
import { RootLoaderData } from 'root';
1516

1617
export function buildAppRootContextValue(
1718
data: AppRootQueryData | null | undefined,
@@ -81,10 +82,15 @@ function AppRoot(): React.JSX.Element {
8182
const navigationBarRef = useRef<HTMLElement>(null);
8283

8384
const [stripePromise, setStripePromise] = useState<Promise<Stripe | null> | null>(null);
85+
const rootLoaderData = useRouteLoaderData('root') as RootLoaderData | undefined;
8486

8587
useEffect(() => {
86-
initErrorReporting(data.currentUser?.id);
87-
}, [data?.currentUser?.id]);
88+
initErrorReporting(
89+
data.currentUser?.id,
90+
rootLoaderData?.clientConfiguration?.sentry_frontend_dsn,
91+
rootLoaderData?.clientConfiguration?.rollbar_client_access_token,
92+
);
93+
}, [data?.currentUser?.id, rootLoaderData?.clientConfiguration]);
8894

8995
useEffect(() => {
9096
reloadOnAppEntrypointHeadersMismatch();

app/javascript/ErrorReporting.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ class ErrorReporting {
2525
return ErrorReporting.instance!;
2626
}
2727

28-
constructor(currentUserId: string | undefined) {
29-
if (window.sentryFrontendDSN) {
28+
constructor(currentUserId: string | undefined, sentryDsn?: string | null, rollbarToken?: string | null) {
29+
if (sentryDsn) {
3030
import('@sentry/browser').then((Sentry) => {
31-
const instance = Sentry.init({ dsn: window.sentryFrontendDSN });
31+
const instance = Sentry.init({ dsn: sentryDsn });
3232
if (instance != null) {
3333
this.sentry = {
3434
instance,
@@ -42,10 +42,10 @@ class ErrorReporting {
4242
});
4343
}
4444

45-
if (window.rollbarClientAccessToken) {
45+
if (rollbarToken) {
4646
import('rollbar').then((Rollbar) => {
4747
this.rollbar = Rollbar.default.init({
48-
accessToken: window.rollbarClientAccessToken,
48+
accessToken: rollbarToken,
4949
captureUncaught: true,
5050
captureUnhandledRejections: true,
5151
captureIp: 'anonymize',

app/javascript/packs/application.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ const bootstrapPromise: Promise<Bootstrap> = (async () => {
6262
window.location.href = redirectUrl.toString();
6363
});
6464

65+
// Make the CDN host available for lazy chunk loading (renderBuiltUrl reads this at call time).
66+
if (clientConfiguration.assets_host) {
67+
window.intercodeAssetsHost = clientConfiguration.assets_host;
68+
}
69+
6570
return { clientConfiguration, authenticityTokensManager, authManager, client };
6671
})();
6772

@@ -73,6 +78,7 @@ function DataModeApplicationEntry() {
7378
createBrowserRouter(
7479
[
7580
{
81+
id: 'root',
7682
lazy: () => import('root'),
7783
children: appRootRoutes,
7884
},

app/javascript/root.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import AuthenticityTokensManager, { AuthenticityTokensContext } from 'Authentici
1111
import { StrictMode } from 'react';
1212
import { LoaderFunction, useLoaderData } from 'react-router';
1313

14-
type RootLoaderData = {
14+
export type RootLoaderData = {
1515
clientConfiguration: ClientConfiguration;
1616
authenticityTokensManager: AuthenticityTokensManager;
1717
client: ApolloClient;

0 commit comments

Comments
 (0)