diff --git a/src/iac/lib/stacks/cloudfront-url-rewrite.js b/src/iac/lib/stacks/cloudfront-url-rewrite.js index 58063c83..1efe0a2c 100644 --- a/src/iac/lib/stacks/cloudfront-url-rewrite.js +++ b/src/iac/lib/stacks/cloudfront-url-rewrite.js @@ -1,7 +1,3 @@ -// CloudFront Function for URL rewriting -// Compatible with CloudFront Functions runtime (ES5.1) -// Query strings are automatically preserved by CloudFront - function handler(event) { var req = event.request; var uri = req.uri; @@ -48,22 +44,71 @@ function handler(event) { return false; } + function getQueryString(querystring) { + if (typeof querystring === "string") { + return querystring ? "?" + querystring : ""; + } + + var parts = []; + for (var key in querystring) { + if (Object.prototype.hasOwnProperty.call(querystring, key)) { + var parameter = querystring[key]; + if (parameter == null || typeof parameter !== "object") { + continue; + } + var values = Array.isArray(parameter.multiValue) + ? parameter.multiValue + : [parameter]; + + for (var i = 0; i < values.length; i++) { + var entry = values[i]; + if (entry == null || typeof entry !== "object") { + continue; + } + var hasValue = Object.prototype.hasOwnProperty.call( + entry, + "value", + ); + parts.push( + encodeURIComponent(key) + + (hasValue ? "=" + encodeURIComponent(entry.value) : ""), + ); + } + } + } + + return parts.length ? "?" + parts.join("&") : ""; + } + + function redirect(path) { + return { + statusCode: 301, + statusDescription: "Moved Permanently", + headers: { + location: { + value: path + getQueryString(req.querystring), + }, + }, + }; + } + + if (uri === "/sitemap.xml") { + return redirect("/sitemap-index.xml"); + } + // Exclude API routes var lower = uri.toLowerCase(); if (hasPrefix(lower, "/api/")) { return req; // no changes } - // Remove trailing slash (except homepage) - if (uri !== "/" && endsWith(uri, "/")) { - uri = uri.substring(0, uri.length - 1); - lower = uri.toLowerCase(); // recompute after modifying uri - } - - // Append /index.html if not homepage and no known extension - // Astro generates directory-based URLs: /docs → /docs/index.html + // Astro generates directory-based URLs with trailing slashes. if (uri !== "/" && !hasKnownExt(lower)) { - uri += "/index.html"; + if (!endsWith(uri, "/")) { + return redirect(uri + "/"); + } + + uri += "index.html"; } req.uri = uri; diff --git a/src/iac/lib/stacks/staticWebsiteStack.ts b/src/iac/lib/stacks/staticWebsiteStack.ts index 0b011df1..ef979a4d 100644 --- a/src/iac/lib/stacks/staticWebsiteStack.ts +++ b/src/iac/lib/stacks/staticWebsiteStack.ts @@ -134,15 +134,15 @@ export class StaticWebsiteStack extends CustomStack { const errorResponse403: ErrorResponse = { httpStatus: 403, - responseHttpStatus: 200, - responsePagePath: "/index.html", + responseHttpStatus: 404, + responsePagePath: "/404.html", ttl: Duration.seconds(10), }; const errorResponse404: ErrorResponse = { httpStatus: 404, - responseHttpStatus: 200, - responsePagePath: "/index.html", + responseHttpStatus: 404, + responsePagePath: "/404.html", ttl: Duration.seconds(10), }; diff --git a/src/website/public/llms.txt b/src/website/public/llms.txt new file mode 100644 index 00000000..a167896b --- /dev/null +++ b/src/website/public/llms.txt @@ -0,0 +1,70 @@ +# Envilder + +> Envilder is MIT-licensed, open-source mapping and secret-resolution tooling for AWS SSM Parameter Store and Azure Key Vault. One `envilder.json` mapping contract is used across local development, CI/CD, and application startup. + +- Scope: Envilder is a mapping and resolution layer, not a secret manager, hosted SaaS, proxy, or control plane. The cloud provider remains the source of truth; Envilder has no hosted secret store or proxy. +- CLI: pulls mapped values to `.env` and supports intentional controlled push. +- GitHub Action: pulls mapped values to `.env` in CI/CD. +- Runtime SDKs: .NET, Python, and Node.js resolve values in-process and can return them or inject process environment variables. The .NET SDK also integrates with configuration and dependency injection APIs. +- Best fit: small teams, projects, or straightforward applications already using SSM or Key Vault that want a version-controlled, PR-reviewable mapping without a separate hosted service. Container and Kubernetes workloads can use runtime SDKs with provider-native identity, but this is application-side resolution, not a native Kubernetes Secret sync, operator, or controller. +- It is not the primary fit when the required backend is unsupported, a hosted or full secret-management control plane is needed, or native Kubernetes secret synchronization is required. +- Google Cloud Secret Manager and AWS Secrets Manager are roadmap items; neither is shipped or scheduled. Request unsupported providers or integrations through the [GitHub issue form](https://github.com/macalbert/envilder/issues/new); requests help prioritize work but do not guarantee implementation. +- Typical CLI flow: configure cloud credentials, keep secret values in the supported cloud store, commit only names and paths in `envilder.json`, then run `npx envilder --map=envilder.json --envfile=.env`. Use the GitHub Action for CI/CD or a runtime SDK for in-process resolution. + +Minimal AWS SSM map: + +```json +{ + "$schema": "https://envilder.com/schema/map-file.v1.json", + "DB_PASSWORD": "/my-app/prod/db-password" +} +``` + +## Start here + +- [Product overview](https://envilder.com/): Envilder's website and product introduction. +- [Website documentation](https://envilder.com/docs/): Documentation hub. +- [Source repository](https://github.com/macalbert/envilder): Project source, issues, releases, and contribution history. +- [Canonical README](https://raw.githubusercontent.com/macalbert/envilder/main/README.md): Primary repository overview in Markdown. +- [Map-file JSON Schema](https://envilder.com/schema/map-file.v1.json): Schema for `envilder.json`. + +## CLI and CI/CD + +- [Installation requirements](https://raw.githubusercontent.com/macalbert/envilder/main/docs/requirements-installation.md): CLI prerequisites and installation guidance. +- [Pull command guide](https://raw.githubusercontent.com/macalbert/envilder/main/docs/pull-command.md): Resolve mapped values into a `.env` file. +- [Push command guide](https://raw.githubusercontent.com/macalbert/envilder/main/docs/push-command.md): Intentional controlled CLI push workflows. +- [GitHub Action documentation](https://raw.githubusercontent.com/macalbert/envilder/main/github-action/README.md): Pull-only CI/CD action usage. + +## Runtime SDKs + +- [.NET SDK documentation](https://raw.githubusercontent.com/macalbert/envilder/main/src/sdks/dotnet/README.md): Runtime resolution for .NET applications. +- [Python SDK documentation](https://raw.githubusercontent.com/macalbert/envilder/main/src/sdks/python/README.md): Runtime resolution for Python applications. +- [Node.js SDK documentation](https://raw.githubusercontent.com/macalbert/envilder/main/src/sdks/nodejs/README.md): Runtime resolution for Node.js applications. + +## Published packages + +- [CLI on npm](https://www.npmjs.com/package/envilder): Install the CLI package `envilder`. +- [.NET SDK on NuGet](https://www.nuget.org/packages/Envilder/): Install the .NET runtime SDK package `Envilder`. +- [Python SDK on PyPI](https://pypi.org/project/envilder/): Install the Python runtime SDK package `envilder`. +- [Node.js SDK on npm](https://www.npmjs.com/package/@envilder/sdk): Install the Node.js runtime SDK package `@envilder/sdk`. + +## Examples + +- [Examples README](https://raw.githubusercontent.com/macalbert/envilder-examples/main/README.md): Reproducible .NET, Python, and TypeScript examples using Testcontainers and LocalStack, plus .NET and TypeScript Aspire examples; paths, not values, are committed. +- [Examples repository](https://github.com/macalbert/envilder-examples): Source for the supported example scope above. + +## Project status and contribution + +- [Roadmap](https://raw.githubusercontent.com/macalbert/envilder/main/ROADMAP.md): Planned and in-progress work; roadmap items are not release commitments. +- [Changelog](https://envilder.com/changelog/): Released changes. +- [MIT license](https://raw.githubusercontent.com/macalbert/envilder/main/LICENSE): Terms for using, modifying, and distributing Envilder. +- [Security policy](https://raw.githubusercontent.com/macalbert/envilder/main/docs/SECURITY.md): Vulnerability reporting guidance. +- [Contributing guide](https://raw.githubusercontent.com/macalbert/envilder/main/CONTRIBUTING.md): Contribution process and local development guidance. +- [Feature request or issue](https://github.com/macalbert/envilder/issues/new): Request an unsupported provider or integration, or report a problem. + +## Optional + +- [LocalStack token hygiene](https://dev.to/macalbert/how-i-run-localstack-without-committing-localstackauthtoken-34gk): Maintainer-authored supplementary article on keeping `LOCALSTACK_AUTH_TOKEN` out of commits. +- [LocalStack integration tests without env files](https://dev.to/macalbert/localstack-integration-tests-without-env-files-b75): Maintainer-authored supplementary article. +- [One year of Envilder](https://dev.to/macalbert/one-year-of-envilder-from-a-cli-script-to-sdks-push-mode-and-a-website-h49): Maintainer-authored supplementary retrospective. +- [Original AWS SSM CLI workflow](https://dev.to/macalbert/stop-hardcoding-secrets-generate-env-files-from-aws-ssm-with-a-simple-cli-f77): Maintainer-authored supplementary historical context documenting the original AWS SSM CLI workflow. diff --git a/src/website/public/robots.txt b/src/website/public/robots.txt new file mode 100644 index 00000000..bd7c254f --- /dev/null +++ b/src/website/public/robots.txt @@ -0,0 +1,4 @@ +User-agent: * +Allow: / + +Sitemap: https://envilder.com/sitemap-index.xml diff --git a/src/website/src/i18n/ca.ts b/src/website/src/i18n/ca.ts index 72257e2b..d4398e73 100644 --- a/src/website/src/i18n/ca.ts +++ b/src/website/src/i18n/ca.ts @@ -3,9 +3,16 @@ import type { Translations } from './types'; export const ca: Translations = { homeMeta: { title: - 'Envilder: estandarditza com les teves aplicacions consumeixen secrets a cada entorn i runtime.', + 'Gestió de secrets open source per a AWS SSM i Azure Key Vault | Envilder', description: - "Defineix el contracte d'entorn una vegada i resol-lo de forma consistent en desenvolupament local, CI/CD i runtime. Amb AWS SSM Parameter Store i Azure Key Vault.", + "Envilder és una CLI, GitHub Action i SDK de runtime open source per carregar secrets des d'AWS SSM Parameter Store i Azure Key Vault.", + }, + notFound: { + title: 'Pàgina no trobada | Envilder', + description: "La pàgina d'Envilder sol·licitada no existeix.", + heading: 'No hem trobat aquesta pàgina', + body: "L'adreça pot ser incorrecta o la pàgina pot haver-se mogut.", + home: 'Torna a l’inici', }, nav: { features: 'Funcionalitats', @@ -451,7 +458,9 @@ export const ca: Translations = { builtWith: 'Fet amb Astro. Codi obert a GitHub.', }, changelogPage: { - title: 'Changelog Envilder | Versions i actualitzacions', + title: 'Canvis d’Envilder: versions de la CLI, GitHub Action i SDKs', + description: + "Historial de versions de la CLI, GitHub Action i SDKs de runtime .NET, Python i Node.js d'Envilder.", backToHome: "← Tornar a l'inici", fullChangelog: 'Historial de ', @@ -468,7 +477,9 @@ export const ca: Translations = { categorySdkNodejs: 'Node.js', }, docs: { - title: 'Docs Envilder | CLI, GitHub Action i AWS SSM', + title: 'Docs Envilder: secrets amb AWS SSM i Azure Key Vault', + description: + "Aprèn a resoldre variables d'entorn des d'AWS SSM Parameter Store i Azure Key Vault amb la CLI, GitHub Action i SDKs de runtime d'Envilder.", backToHome: "← Tornar a l'inici", pageTitle: 'Documentació', diff --git a/src/website/src/i18n/en.ts b/src/website/src/i18n/en.ts index 531546b7..c0073dd0 100644 --- a/src/website/src/i18n/en.ts +++ b/src/website/src/i18n/en.ts @@ -3,9 +3,16 @@ import type { Translations } from './types'; export const en: Translations = { homeMeta: { title: - 'Envilder: standardize how your applications consume secrets across every environment and runtime.', + 'Open-source secret management for AWS SSM and Azure Key Vault | Envilder', description: - 'Define your environment contract once and resolve it consistently across local development, CI/CD, and runtime. Using AWS SSM Parameter Store and Azure Key Vault.', + 'Envilder is an open-source CLI, GitHub Action, and runtime SDK for loading secrets from AWS SSM Parameter Store and Azure Key Vault.', + }, + notFound: { + title: 'Page not found | Envilder', + description: 'The requested Envilder page does not exist.', + heading: 'This page was not found', + body: 'The address may be incorrect, or the page may have moved.', + home: 'Return home', }, nav: { features: 'Features', @@ -450,7 +457,9 @@ export const en: Translations = { builtWith: 'Built with Astro. Open source on GitHub.', }, changelogPage: { - title: 'Envilder Changelog | Releases & Updates', + title: 'Envilder changelog: CLI, GitHub Action, and SDK releases', + description: + 'Release history for the Envilder CLI, GitHub Action, and .NET, Python, and Node.js runtime SDKs.', backToHome: '← Back to home', fullChangelog: 'Full ', @@ -467,7 +476,9 @@ export const en: Translations = { categorySdkNodejs: 'Node.js', }, docs: { - title: 'Envilder Docs | CLI, GitHub Action & AWS SSM', + title: 'Envilder docs: AWS SSM and Azure Key Vault secrets', + description: + 'Learn how to resolve environment variables from AWS SSM Parameter Store and Azure Key Vault with the Envilder CLI, GitHub Action, and runtime SDKs.', backToHome: '← Back to home', pageTitle: 'Documentation', diff --git a/src/website/src/i18n/es.ts b/src/website/src/i18n/es.ts index 938d7a4f..59c1e9a7 100644 --- a/src/website/src/i18n/es.ts +++ b/src/website/src/i18n/es.ts @@ -3,9 +3,16 @@ import type { Translations } from './types'; export const es: Translations = { homeMeta: { title: - 'Envilder: estandariza cómo tus aplicaciones consumen secretos en cada entorno y runtime.', + 'Gestión de secretos open source para AWS SSM y Azure Key Vault | Envilder', description: - 'Define tu contrato de entorno una vez y resuélvelo de forma consistente en desarrollo local, CI/CD y runtime. Con AWS SSM Parameter Store y Azure Key Vault.', + 'Envilder es una CLI, GitHub Action y SDK de runtime open source para cargar secretos desde AWS SSM Parameter Store y Azure Key Vault.', + }, + notFound: { + title: 'Página no encontrada | Envilder', + description: 'La página de Envilder solicitada no existe.', + heading: 'No hemos encontrado esta página', + body: 'La dirección puede ser incorrecta o la página puede haberse movido.', + home: 'Volver al inicio', }, nav: { features: 'Funcionalidades', @@ -451,7 +458,9 @@ export const es: Translations = { builtWith: 'Hecho con Astro. Código abierto en GitHub.', }, changelogPage: { - title: 'Changelog Envilder | Versiones y actualizaciones', + title: 'Cambios de Envilder: versiones de la CLI, GitHub Action y SDKs', + description: + 'Historial de versiones de la CLI, GitHub Action y SDKs de runtime .NET, Python y Node.js de Envilder.', backToHome: '← Volver al inicio', fullChangelog: 'Historial de ', @@ -468,7 +477,9 @@ export const es: Translations = { categorySdkNodejs: 'Node.js', }, docs: { - title: 'Docs Envilder | CLI, GitHub Action y AWS SSM', + title: 'Docs Envilder: secretos con AWS SSM y Azure Key Vault', + description: + 'Aprende a resolver variables de entorno desde AWS SSM Parameter Store y Azure Key Vault con la CLI, GitHub Action y SDKs de runtime de Envilder.', backToHome: '← Volver al inicio', pageTitle: 'Documentación', diff --git a/src/website/src/i18n/types.ts b/src/website/src/i18n/types.ts index 6403251c..39fd820d 100644 --- a/src/website/src/i18n/types.ts +++ b/src/website/src/i18n/types.ts @@ -253,6 +253,7 @@ export interface FooterTranslations { export interface ChangelogPageTranslations { title: string; + description: string; backToHome: string; fullChangelog: string; changelogAccent: string; @@ -270,6 +271,7 @@ export interface ChangelogPageTranslations { export interface DocsTranslations { title: string; + description: string; backToHome: string; pageTitle: string; intro: string; @@ -533,6 +535,14 @@ export interface HomeMetaTranslations { description: string; } +export interface NotFoundTranslations { + title: string; + description: string; + heading: string; + body: string; + home: string; +} + export interface SponsorsTranslations { title: string; localstackAlt: string; @@ -542,6 +552,7 @@ export interface SponsorsTranslations { export interface Translations { homeMeta: HomeMetaTranslations; + notFound: NotFoundTranslations; nav: NavLinks; theme: ThemeTranslations; hero: HeroTranslations; diff --git a/src/website/src/layouts/BaseLayout.astro b/src/website/src/layouts/BaseLayout.astro index d2b270d0..3c990507 100644 --- a/src/website/src/layouts/BaseLayout.astro +++ b/src/website/src/layouts/BaseLayout.astro @@ -1,14 +1,18 @@ --- +import { defaultLang, languages } from '../i18n/utils'; + export interface Props { title?: string; description?: string; lang?: string; + noindex?: boolean; } const { title = 'Envilder: centralize your secrets. One command.', description = 'A CLI tool and GitHub Action that securely centralizes environment variables from AWS SSM Parameter Store or Azure Key Vault as a single source of truth.', lang = 'en', + noindex = false, } = Astro.props; const skipLabel: Record = { @@ -18,7 +22,82 @@ const skipLabel: Record = { }; const skipText = skipLabel[lang] ?? skipLabel.en; -const canonicalUrl = new URL(Astro.url.pathname, Astro.site).href; +const siteUrl = Astro.site ?? 'https://envilder.com'; +const canonicalUrl = new URL(Astro.url.pathname, siteUrl).href; +const siteRootUrl = new URL('/', siteUrl).href; +const alternateLanguages = Object.keys(languages); +const localizedLanguages = alternateLanguages.filter( + (language) => language !== defaultLang, +); +const localizedLanguagePattern = localizedLanguages.join('|'); +const pathWithoutLocale = localizedLanguagePattern + ? Astro.url.pathname.replace( + new RegExp(`^/(?:${localizedLanguagePattern})(?=/|$)`), + '', + ) || '/' + : Astro.url.pathname; +const defaultLanguageUrl = new URL(pathWithoutLocale, siteUrl).href; +const alternateLanguageUrls = alternateLanguages.map((language) => ({ + language, + href: new URL( + language === defaultLang + ? pathWithoutLocale + : `/${language}${pathWithoutLocale}`, + siteUrl, + ).href, +})); +const isHomePage = pathWithoutLocale === '/'; +const structuredData = JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [ + { + '@type': 'Organization', + '@id': `${siteRootUrl}#organization`, + name: 'Envilder', + url: siteRootUrl, + sameAs: ['https://github.com/macalbert/envilder'], + }, + { + '@type': 'WebSite', + '@id': `${siteRootUrl}#website`, + name: 'Envilder', + url: siteRootUrl, + publisher: { + '@id': `${siteRootUrl}#organization`, + }, + }, + ...(!noindex + ? [ + { + '@type': 'WebPage', + '@id': `${canonicalUrl}#webpage`, + url: canonicalUrl, + name: title, + description, + inLanguage: lang, + isPartOf: { + '@id': `${siteRootUrl}#website`, + }, + }, + ] + : []), + ...(isHomePage && !noindex + ? [ + { + '@type': 'SoftwareApplication', + '@id': `${siteRootUrl}#software`, + name: 'Envilder', + description, + applicationCategory: 'DeveloperApplication', + operatingSystem: 'Windows, macOS, Linux', + isAccessibleForFree: true, + codeRepository: 'https://github.com/macalbert/envilder', + license: 'https://github.com/macalbert/envilder/blob/main/LICENSE', + }, + ] + : []), + ], +}); --- @@ -29,6 +108,18 @@ const canonicalUrl = new URL(Astro.url.pathname, Astro.site).href; + {noindex && } + + {!noindex && + alternateLanguageUrls.map((alternate) => ( + + ))} + {!noindex && } + diff --git a/src/website/src/pages/404.astro b/src/website/src/pages/404.astro new file mode 100644 index 00000000..08472182 --- /dev/null +++ b/src/website/src/pages/404.astro @@ -0,0 +1,99 @@ +--- +import Footer from '../components/Footer.astro'; +import Navbar from '../components/Navbar.astro'; +import { defaultLang, languages, useTranslations } from '../i18n/utils'; +import BaseLayout from '../layouts/BaseLayout.astro'; + +const lang = defaultLang; +const t = useTranslations(lang); +const notFoundTranslations = Object.fromEntries( + Object.keys(languages).map((locale) => [ + locale, + useTranslations(locale).notFound, + ]), +); +--- + + + +
+
+
+
+

404

+

{t.notFound.heading}

+

{t.notFound.body}

+ {t.notFound.home} +
+
+
+
+