From cce1334a9b03031a27cb29009084b1e5866cde04 Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Mon, 6 Jul 2026 17:10:39 +0100 Subject: [PATCH 1/3] Fix links to pages that moved on the main site outbound/senders became outbound/sources and privacy-and-security became legal/security-and-privacy-policy; the external_links check has been failing on the redirects. --- content/_common_links.md | 2 +- content/other/security.md | 2 +- content/outbound/sending_email_via_json_api.md | 2 +- content/outbound/sending_email_with_smtp.md | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/content/_common_links.md b/content/_common_links.md index f5e39a5..aecc67a 100644 --- a/content/_common_links.md +++ b/content/_common_links.md @@ -3,6 +3,6 @@ [CloudMailin]: https://www.cloudmailin.com [contact us]: https://www.cloudmailin.com/contact_us [signup]: https://www.cloudmailin.com/plans-and-pricing -[outbound account]: https://www.cloudmailin.com/outbound/senders +[outbound account]: https://www.cloudmailin.com/outbound/sources [WebhookApp]: https://www.webhookapp.com [Heroku]: https://www.heroku.com diff --git a/content/other/security.md b/content/other/security.md index 76dd08c..9063a6e 100644 --- a/content/other/security.md +++ b/content/other/security.md @@ -8,5 +8,5 @@ description: The CloudMailin security and privacy policy can be found on the For questions or concerns relating to security please see the security section of our main site: -[https://www.cloudmailin.com/privacy-and-security](https://www.cloudmailin.com/privacy-and-security) +[https://www.cloudmailin.com/legal/security-and-privacy-policy](https://www.cloudmailin.com/legal/security-and-privacy-policy) You can also [contact us] at any time for more information. diff --git a/content/outbound/sending_email_via_json_api.md b/content/outbound/sending_email_via_json_api.md index dd60307..4f5486a 100644 --- a/content/outbound/sending_email_via_json_api.md +++ b/content/outbound/sending_email_via_json_api.md @@ -119,5 +119,5 @@ emails with your own library or migrating from another provider. <%= render_api_fields("components/schemas/RawMessage", include_readonly: false, except: %w[headers attachments]) %> [Client Library]: #client-libraries -[SMTP Accounts]: https://www.cloudmailin.com/outbound/senders +[SMTP Accounts]: https://www.cloudmailin.com/outbound/sources [SMTP]: <%= url_to_item('/outbound/sending_email_with_smtp/') %> diff --git a/content/outbound/sending_email_with_smtp.md b/content/outbound/sending_email_with_smtp.md index efb787f..c75a436 100644 --- a/content/outbound/sending_email_with_smtp.md +++ b/content/outbound/sending_email_with_smtp.md @@ -48,4 +48,4 @@ email over SMTP: * <%= link_to_item(item) %><% end %> -[account page]: https://www.cloudmailin.com/outbound/senders +[account page]: https://www.cloudmailin.com/outbound/sources From 3969044b766db121bde97357c5346723a6457961 Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Mon, 6 Jul 2026 17:10:39 +0100 Subject: [PATCH 2/3] Add a cancelling-your-account doc MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docs search showed people looking for how to cancel without finding anything. Deliberately not linked in the sidebar — it's there for people who search for it. --- .../cancelling_your_account.md | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 content/getting_started/cancelling_your_account.md diff --git a/content/getting_started/cancelling_your_account.md b/content/getting_started/cancelling_your_account.md new file mode 100644 index 0000000..be82d29 --- /dev/null +++ b/content/getting_started/cancelling_your_account.md @@ -0,0 +1,45 @@ +--- +title: Cancelling your Account or Subscription +description: + How to cancel your CloudMailin subscription, delete your addresses or + remove your user account entirely. +--- + +# Cancelling your Account + +You can cancel your CloudMailin subscription at any time from your +[user page](https://www.cloudmailin.com/user). There are two options, +depending on how much you want to remove: + +## Delete all addresses and subscriptions + +The **Delete All Addresses** button on your +[user page](https://www.cloudmailin.com/user) deletes all of your addresses +and cancels any paid subscriptions. You will no longer be billed, but you +can continue to log in and create new addresses later if you need to. + +## Delete your user entirely + +The **Delete User** button removes your user, your account and all +associated resources. You will not be able to log in again afterwards. + +> Both actions take effect immediately and **cannot be undone**. Neither +> deletes resources in an account owned by someone else, such as a team +> account you are a member of. + +## Accounts created through a provider + +If you signed up through a provider such as the Heroku add-on, that +provider handles your billing, so the subscription must be cancelled +there — remove the add-on from your application and the CloudMailin +account will be cancelled automatically. + +## Downgrading instead + +If you want to reduce your bill rather than leave, you can switch to a +smaller plan — including the free plan — from the +[plans page](https://www.cloudmailin.com/plans-and-pricing). See the +[refund policy](https://www.cloudmailin.com/legal/refund-policy) for how +refunds are handled, or +[contact us](https://www.cloudmailin.com/contact_us) if you have any +questions. From 7c696f97cbe0e6e5c8cabde8b07ea3b4ef93e5ea Mon Sep 17 00:00:00 2001 From: Steve Smith Date: Mon, 6 Jul 2026 17:10:39 +0100 Subject: [PATCH 3/3] Index key website pages into docs search Docs search couldn't answer queries for content that lives on the main site (gdpr, dpa, cancel and pricing all returned nothing). A postprocess step now fetches the pages listed under algolia:external_pages in config.yaml and indexes them alongside the docs content, with the page URL as the objectID so results link straight to the site. --- Rules | 6 +++ config.yaml | 11 ++++++ lib/algolia_external.rb | 83 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 lib/algolia_external.rb diff --git a/Rules b/Rules index d663256..69f8c47 100644 --- a/Rules +++ b/Rules @@ -11,6 +11,12 @@ preprocess do Nanoc::Filters::AlgoliaFilter.new(config: config).destroy_index end +postprocess do + next if ENV['NANOC_ENV'] == 'development' + + AlgoliaExternalIndexer.new(config: config).run +end + compile '*' do if item.identifier == '/_common_links/' nil diff --git a/config.yaml b/config.yaml index abe737e..1826eb7 100644 --- a/config.yaml +++ b/config.yaml @@ -86,6 +86,17 @@ checks: - https://www.npmjs.com/package/cloudmailin # cloudflare protection raises 403 - https://docs.cloudmailin.com/features/dmarc-verification/ # self-referential redirect +# Algolia search: pages from the main website fetched and indexed at +# build time alongside the docs content (see lib/algolia_external.rb). +# Application ID, API key and index name come from the ALGOLIA_* env vars. +algolia: + external_pages: + - https://www.cloudmailin.com/legal/security-and-privacy-policy + - https://www.cloudmailin.com/legal/terms-of-service + - https://www.cloudmailin.com/legal/acceptable-use-policy + - https://www.cloudmailin.com/legal/refund-policy + - https://www.cloudmailin.com/plans-and-pricing + # Deploy using fog deploy: default: diff --git a/lib/algolia_external.rb b/lib/algolia_external.rb new file mode 100644 index 0000000..73c3880 --- /dev/null +++ b/lib/algolia_external.rb @@ -0,0 +1,83 @@ +require 'algolia' +require 'net/http' +require 'nokogiri' + +# Fetches a fixed list of pages from the main website (algolia: +# external_pages in config.yaml) and indexes them alongside the docs +# content, so searches for things that live on www.cloudmailin.com +# (legal policies, pricing) return results in the docs search. +class AlgoliaExternalIndexer + # Existing docs records reach ~39KB, so the record-size limit on this + # plan comfortably allows full page text (the longest external page is + # currently ~11K chars — truncating it would drop the GDPR/DPA text + # people actually search for). + MAX_CONTENT_LENGTH = 20_000 + MAX_REDIRECTS = 3 + + def initialize(config:) + @pages = config.dig(:algolia, :external_pages) || [] + + index = config.dig(:algolia, :index) || ENV['ALGOLIA_INDEX'] + @skip_index = index.nil? || index.empty? + return if @skip_index + + app_id = config.dig(:algolia, :application_id) || ENV['ALGOLIA_APPLICATION_ID'] + api_key = config.dig(:algolia, :api_key) || ENV['ALGOLIA_API_KEY'] + + raise ArgumentError, 'Missing algolia:application_id' unless app_id + raise ArgumentError, 'Missing algolia:api_key' unless api_key + + client = Algolia::Search::Client.create(app_id, api_key) + @index = client.init_index(index + "_#{ENV['NANOC_ENV']}") + end + + def run + return if @skip_index + + @pages.each do |url| + record = fetch(url) + if record.nil? + warn "Failed to fetch #{url} - not indexed" + next + end + + @index.save_object( + record.merge(objectID: url, external: true, deprecated: false, no_index: false) + ).wait + puts "Indexed #{url} in Algolia" + end + end + + private + + def fetch(url, redirects_left = MAX_REDIRECTS) + response = Net::HTTP.get_response(URI(url)) + + case response + when Net::HTTPRedirection + return nil if redirects_left.zero? + + fetch(URI.join(url, response['location']).to_s, redirects_left - 1) + when Net::HTTPSuccess + extract(response.body) + end + rescue StandardError => e + warn "#{url}: #{e.class}: #{e.message}" + nil + end + + def extract(html) + doc = Nokogiri::HTML(html) + doc.css('script, style, nav, header, footer, noscript').each(&:remove) + + content = (doc.at_css('main') || doc.at_css('body')) + .text.gsub(/\s+/, ' ').strip[0, MAX_CONTENT_LENGTH] + description = doc.at_css('meta[name="description"]')&.[]('content') + + { + title: doc.at_css('title')&.text.to_s.sub(/\s*[|·—-]\s*CloudMailin.*\z/i, '').strip, + description: description || content[0, 160], + content: content + } + end +end