Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/self-hosted/hosting/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ services:
image: ecency/self-hosted:${TAG:?TAG environment variable is required}
container_name: ecency-hosting-blog
restart: unless-stopped
# The newsletter subscribe location proxies to the origin vhost on this box;
# host-gateway is what makes host.docker.internal resolve on Linux Docker.
extra_hosts:
- "host.docker.internal:host-gateway"
ports:
- "127.0.0.1:3100:80"
volumes:
Expand Down
44 changes: 44 additions & 0 deletions apps/self-hosted/hosting/nginx-multi-tenant.conf
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,28 @@ server {
# Per-tenant static SEO files, written by the hosting API's sync pass.
# robots falls back to the generic file baked in the image; sitemap and
# rss simply 404 until the first pass writes them.
# Newsletter subscribe (vision-web#1537): the blog's signup form posts here,
# same-origin, and this forwards to the web tier THROUGH THE ORIGIN VHOST ON
# THIS BOX (host.docker.internal, an extra_hosts entry in the compose file),
# deliberately not through public ecency.com: a Cloudflare hop would make
# every tenant's signup look like one datacenter client (one IP for consent
# records, one bucket for rate limits, and a bot challenge would break the
# form outright). The reader's address travels as CF-Connecting-IP, set from
# X-Real-IP, which the HOST's edge vhost asserted from its own remote_addr;
# the origin's /api/ location forwards exactly that header pair to the app.
# Managed instances only, by construction: only they sit behind this nginx.
# Only the subscribe POST is exposed; confirmation and unsubscribe links in
# the mail point at ecency.com directly.
location = /api/newsletter/subscribe {
proxy_pass https://host.docker.internal/api/newsletter/subscribe;
proxy_ssl_server_name on;
proxy_ssl_name eu.ecency.com;
proxy_set_header Host eu.ecency.com;
proxy_set_header CF-Connecting-IP $http_x_real_ip;
proxy_read_timeout 15s;
client_max_body_size 16k;
}

location = /robots.txt {
try_files /configs/$tenant_id.robots.txt /robots.txt =404;
}
Expand Down Expand Up @@ -210,6 +232,28 @@ server {
}

# Same static SEO files, keyed by custom tenant.
# Newsletter subscribe (vision-web#1537): the blog's signup form posts here,
# same-origin, and this forwards to the web tier THROUGH THE ORIGIN VHOST ON
# THIS BOX (host.docker.internal, an extra_hosts entry in the compose file),
# deliberately not through public ecency.com: a Cloudflare hop would make
# every tenant's signup look like one datacenter client (one IP for consent
# records, one bucket for rate limits, and a bot challenge would break the
# form outright). The reader's address travels as CF-Connecting-IP, set from
# X-Real-IP, which the HOST's edge vhost asserted from its own remote_addr;
# the origin's /api/ location forwards exactly that header pair to the app.
# Managed instances only, by construction: only they sit behind this nginx.
# Only the subscribe POST is exposed; confirmation and unsubscribe links in
# the mail point at ecency.com directly.
location = /api/newsletter/subscribe {
proxy_pass https://host.docker.internal/api/newsletter/subscribe;
proxy_ssl_server_name on;
proxy_ssl_name eu.ecency.com;
proxy_set_header Host eu.ecency.com;
proxy_set_header CF-Connecting-IP $http_x_real_ip;
proxy_read_timeout 15s;
client_max_body_size 16k;
}

location = /robots.txt {
try_files /configs/$custom_tenant_id.robots.txt /robots.txt =404;
}
Expand Down
2 changes: 2 additions & 0 deletions apps/self-hosted/src/core/configuration-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ export interface InstanceConfig {
post: {
text2Speech: { enabled: boolean };
};
/** The email-digest signup form (managed instances; the form posts to the host's own /api/newsletter/subscribe). */
newsletter?: { enabled?: boolean };
tipping?: {
enabled?: boolean;
general?: { enabled: boolean; buttonLabel?: string };
Expand Down
28 changes: 27 additions & 1 deletion apps/self-hosted/src/core/i18n-strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,20 @@ export type TranslationKey =
| 'panel_configuration_instance_configuration_features_hive_payout_label_description'
| 'panel_validation_learn_more_url'
| 'panel_validation_create_post_url_community'
| 'panel_validation_create_post_url_refused';
| 'panel_validation_create_post_url_refused'
| 'newsletterTitle'
| 'newsletterBlurb'
| 'newsletterCommunityBlurb'
| 'newsletterEmail'
| 'newsletterWeekly'
| 'newsletterMonthly'
| 'newsletterCadence'
| 'newsletterSubscribe'
| 'newsletterCheckInbox'
| 'newsletterError'
| 'panel_configuration_instance_configuration_features_newsletter_label'
| 'panel_configuration_instance_configuration_features_newsletter_enabled_label'
| 'panel_configuration_instance_configuration_features_newsletter_enabled_description';

export type Translations = Record<TranslationKey, string>;

Expand All @@ -323,6 +336,19 @@ export const translations: { en: Translations } & Record<
Partial<Translations>
> = {
en: {
panel_configuration_instance_configuration_features_newsletter_label: 'Newsletter signup',
panel_configuration_instance_configuration_features_newsletter_enabled_label: 'Show the email signup form',
panel_configuration_instance_configuration_features_newsletter_enabled_description: 'Readers can subscribe to a weekly or monthly email digest of this site (managed hosting only; double opt-in).',
newsletterTitle: 'Get new posts by email',
newsletterBlurb: 'A weekly or monthly digest of new posts. Double opt-in, unsubscribe any time.',
newsletterCommunityBlurb: 'The best of this community as a weekly or monthly digest. Double opt-in, unsubscribe any time.',
newsletterEmail: 'Your email',
newsletterWeekly: 'Weekly',
newsletterMonthly: 'Monthly',
newsletterCadence: 'How often',
newsletterSubscribe: 'Subscribe',
newsletterCheckInbox: 'Almost there: confirm from the email we just sent.',
newsletterError: 'Could not subscribe right now. Please try again.',
loading: "Loading...",
hivesigner_login_failed: 'Sign in could not be completed. Please try again.',
loadingPost: "Loading post...",
Expand Down
1 change: 1 addition & 0 deletions apps/self-hosted/src/features/blog/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export * from './blog-post-item';
export * from './blog-post-page';
export * from './blog-posts-list';
export * from './detect-bottom';
export * from './newsletter-signup';
Loading
Loading