master 0cd68b5df9ee cached
527 files
2.4 MB
643.7k tokens
177 symbols
2 requests
Download .txt
Showing preview only (2,567K chars total). Download the full file or copy to clipboard to get everything.
Repository: endoflife-date/endoflife.date
Branch: master
Commit: 0cd68b5df9ee
Files: 527
Total size: 2.4 MB

Directory structure:
gitextract_evhxaady/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── feature_request.md
│   │   ├── new_product_suggestion.md
│   │   └── report_incorrect_details.md
│   ├── config.yml
│   ├── copilot-instructions.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── auto-merge-release-updates.yml
│       ├── check-links.yml
│       └── lint.yml
├── .gitignore
├── .gitmodules
├── .markdownlint.yaml
├── .prettierignore
├── .prettierrc
├── .ruby-version
├── .vacuumignore.yml
├── 404.html
├── CHANGELOG_API.md
├── CODE-OF-CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── HACKING.md
├── LICENSE
├── README.md
├── _config.yml
├── _headers
├── _includes/
│   ├── css/
│   │   └── activation.scss.liquid
│   ├── custom-column-td.html
│   ├── custom-column-th.html
│   ├── head_custom.html
│   ├── identifiers.html
│   ├── lunr/
│   │   ├── custom-data.json
│   │   └── custom-index.js
│   ├── nav_footer_custom.html
│   ├── product-icon.html
│   ├── table.html
│   └── variables.html
├── _layouts/
│   ├── json.json
│   ├── new-products-feed.atom
│   ├── page.html
│   ├── product-feed.atom
│   ├── product-list.html
│   ├── product-tags.html
│   ├── product.html
│   ├── schema.html
│   └── swagger-ui.html
├── _plugins/
│   ├── create-icalendar-files.rb
│   ├── end-of-life-filters.rb
│   ├── end-of-life.rb
│   ├── generate-api-v0.rb
│   ├── generate-api-v1.rb
│   ├── generate-product-feeds.rb
│   ├── generate-tag-pages.rb
│   ├── identifier-to-url.rb
│   ├── product-data-enricher.rb
│   └── product-data-validator.rb
├── _redirects
├── _sass/
│   └── custom/
│       └── custom.scss
├── api_v1/
│   ├── openapi.yml
│   └── swagger-ui.md
├── assets/
│   ├── main.scss
│   ├── openapi.yml
│   └── register-show-hidden-releases-handler.js
├── bin/
│   ├── deploy.sh
│   ├── lint-product.sh
│   └── update_added_at.sh
├── bot.md
├── browserconfig.xml
├── humans.txt
├── index.md
├── manifest.json
├── netlify.toml
├── package.json
├── pages/
│   └── help/
│       └── identifiers-needed.md
├── product-schema.json
├── products/
│   ├── adonisjs.md
│   ├── akeneo-pim.md
│   ├── alibaba-ack.md
│   ├── alibaba-dragonwell.md
│   ├── almalinux.md
│   ├── alpine-linux.md
│   ├── amazon-aurora-postgresql.md
│   ├── amazon-cdk.md
│   ├── amazon-corretto.md
│   ├── amazon-documentdb.md
│   ├── amazon-eks.md
│   ├── amazon-elasticache-redis.md
│   ├── amazon-glue.md
│   ├── amazon-linux.md
│   ├── amazon-msk.md
│   ├── amazon-neptune.md
│   ├── amazon-opensearch.md
│   ├── amazon-rds-mariadb.md
│   ├── amazon-rds-mysql.md
│   ├── amazon-rds-postgresql.md
│   ├── android.md
│   ├── angular.md
│   ├── angularjs.md
│   ├── ansible-core.md
│   ├── ansible.md
│   ├── anthropic-claude.md
│   ├── antix-linux.md
│   ├── apache-activemq-artemis.md
│   ├── apache-activemq.md
│   ├── apache-airflow.md
│   ├── apache-ant.md
│   ├── apache-apisix.md
│   ├── apache-camel.md
│   ├── apache-cassandra.md
│   ├── apache-couchdb.md
│   ├── apache-flink.md
│   ├── apache-groovy.md
│   ├── apache-hadoop.md
│   ├── apache-hop.md
│   ├── apache-http-server.md
│   ├── apache-kafka.md
│   ├── apache-lucene.md
│   ├── apache-maven.md
│   ├── apache-nifi.md
│   ├── apache-pulsar.md
│   ├── apache-spark.md
│   ├── apache-struts.md
│   ├── apache-subversion.md
│   ├── api-platform.md
│   ├── apple-tvos.md
│   ├── apple-watch.md
│   ├── arangodb.md
│   ├── argo-cd.md
│   ├── argo-workflows.md
│   ├── artifactory.md
│   ├── authentik.md
│   ├── aws-lambda.md
│   ├── azul-zulu.md
│   ├── azure-devops-server.md
│   ├── azure-kubernetes-service.md
│   ├── backdrop.md
│   ├── bamboo.md
│   ├── bazel.md
│   ├── beats.md
│   ├── behat.md
│   ├── bellsoft-liberica.md
│   ├── big-ip.md
│   ├── bigbluebutton.md
│   ├── bitbucket.md
│   ├── bitcoin-core.md
│   ├── blender.md
│   ├── bootstrap.md
│   ├── boundary.md
│   ├── bun.md
│   ├── cachet.md
│   ├── caddy.md
│   ├── cakephp.md
│   ├── calico.md
│   ├── centos-stream.md
│   ├── centos.md
│   ├── centreon.md
│   ├── cert-manager.md
│   ├── cfengine.md
│   ├── chef-infra-client.md
│   ├── chef-infra-server.md
│   ├── chef-inspec.md
│   ├── chef-supermarket.md
│   ├── chef-workstation.md
│   ├── chrome.md
│   ├── cilium.md
│   ├── cisco-ios-xe.md
│   ├── citrix-vad.md
│   ├── ckeditor.md
│   ├── clamav.md
│   ├── clearlinux.md
│   ├── cloud-sql-auth-proxy.md
│   ├── cnspec.md
│   ├── cockroachdb.md
│   ├── coder.md
│   ├── coldfusion.md
│   ├── commvault.md
│   ├── composer.md
│   ├── concrete-cms.md
│   ├── confluence.md
│   ├── consul.md
│   ├── containerd.md
│   ├── contao.md
│   ├── contour.md
│   ├── controlm.md
│   ├── cos.md
│   ├── couchbase-server.md
│   ├── craft-cms.md
│   ├── dbt-core.md
│   ├── dce.md
│   ├── debian.md
│   ├── deno.md
│   ├── dependency-track.md
│   ├── devuan.md
│   ├── discourse.md
│   ├── django.md
│   ├── docker-engine.md
│   ├── dotnet.md
│   ├── dotnetfx.md
│   ├── dovecot.md
│   ├── drupal.md
│   ├── drush.md
│   ├── duckdb.md
│   ├── eclipse-jetty.md
│   ├── eclipse-temurin.md
│   ├── elasticsearch.md
│   ├── electron.md
│   ├── elixir.md
│   ├── emberjs.md
│   ├── envoy.md
│   ├── erlang.md
│   ├── eslint.md
│   ├── etcd.md
│   ├── eurolinux.md
│   ├── exim.md
│   ├── express.md
│   ├── fairphone.md
│   ├── fedora.md
│   ├── ffmpeg.md
│   ├── filemaker.md
│   ├── firefox.md
│   ├── fluent-bit.md
│   ├── flux.md
│   ├── font-awesome.md
│   ├── foreman.md
│   ├── forgejo.md
│   ├── fortios.md
│   ├── freebsd.md
│   ├── freedesktop-sdk.md
│   ├── gatekeeper.md
│   ├── gerrit.md
│   ├── ghc.md
│   ├── gitlab.md
│   ├── gleam.md
│   ├── go.md
│   ├── goaccess.md
│   ├── godot.md
│   ├── google-kubernetes-engine.md
│   ├── google-nexus.md
│   ├── gorilla.md
│   ├── graalvm-ce.md
│   ├── gradle.md
│   ├── grafana-loki.md
│   ├── grafana.md
│   ├── grails.md
│   ├── graylog.md
│   ├── greenlight.md
│   ├── grumphp.md
│   ├── grunt.md
│   ├── gstreamer.md
│   ├── guzzle.md
│   ├── haproxy.md
│   ├── harbor.md
│   ├── hashicorp-packer.md
│   ├── hashicorp-vault.md
│   ├── hbase.md
│   ├── hibernate-orm.md
│   ├── ibm-aix.md
│   ├── ibm-db2.md
│   ├── ibm-i.md
│   ├── ibm-mq.md
│   ├── ibm-semeru.md
│   ├── icinga-web.md
│   ├── icinga.md
│   ├── idl.md
│   ├── influxdb.md
│   ├── intel-processors.md
│   ├── internet-explorer.md
│   ├── ionic.md
│   ├── ios.md
│   ├── ipad.md
│   ├── ipados.md
│   ├── iphone.md
│   ├── isc-dhcp.md
│   ├── istio.md
│   ├── jaeger.md
│   ├── jekyll.md
│   ├── jenkins.md
│   ├── jhipster.md
│   ├── jira-software.md
│   ├── joomla.md
│   ├── jquery-ui.md
│   ├── jquery.md
│   ├── jreleaser.md
│   ├── julia.md
│   ├── karpenter.md
│   ├── kde-plasma.md
│   ├── keda.md
│   ├── keycloak.md
│   ├── kibana.md
│   ├── kindle.md
│   ├── kirby.md
│   ├── knative.md
│   ├── kong-gateway.md
│   ├── kotlin.md
│   ├── kubernetes-csi-node-driver-registrar.md
│   ├── kubernetes-node-feature-discovery.md
│   ├── kubernetes.md
│   ├── kuma.md
│   ├── kyverno.md
│   ├── laravel.md
│   ├── ldap-account-manager.md
│   ├── libreoffice.md
│   ├── lineageos.md
│   ├── linux-kernel.md
│   ├── linuxmint.md
│   ├── liquibase.md
│   ├── log4j.md
│   ├── logstash.md
│   ├── longhorn.md
│   ├── looker.md
│   ├── lua.md
│   ├── macos.md
│   ├── mageia.md
│   ├── magento.md
│   ├── mandrel.md
│   ├── mariadb.md
│   ├── mastodon.md
│   ├── matomo.md
│   ├── mattermost.md
│   ├── mautic.md
│   ├── mediawiki.md
│   ├── meilisearch.md
│   ├── memcached.md
│   ├── micronaut.md
│   ├── microsoft-build-of-openjdk.md
│   ├── mongodb.md
│   ├── moodle.md
│   ├── motorola-mobility.md
│   ├── msexchange.md
│   ├── mssqlserver.md
│   ├── mule-runtime.md
│   ├── mxlinux.md
│   ├── mysql.md
│   ├── neo4j.md
│   ├── neos.md
│   ├── netapp-ontap.md
│   ├── netbackup-appliance-os.md
│   ├── netbsd.md
│   ├── nextcloud.md
│   ├── nextjs.md
│   ├── nexus.md
│   ├── nginx.md
│   ├── nix.md
│   ├── nixos.md
│   ├── nodejs.md
│   ├── nokia.md
│   ├── nomad.md
│   ├── notepad-plus-plus.md
│   ├── numpy.md
│   ├── nutanix-aos.md
│   ├── nutanix-files.md
│   ├── nutanix-prism.md
│   ├── nuxt.md
│   ├── nvidia-driver.md
│   ├── nvidia-gpu.md
│   ├── nvm.md
│   ├── office.md
│   ├── omnissa-horizon.md
│   ├── oneplus.md
│   ├── openbao.md
│   ├── openbsd.md
│   ├── openjdk-builds-from-oracle.md
│   ├── opensearch.md
│   ├── openssl.md
│   ├── opensuse.md
│   ├── opentofu.md
│   ├── openvpn.md
│   ├── openwrt.md
│   ├── openzfs.md
│   ├── opnsense.md
│   ├── oracle-apex.md
│   ├── oracle-database.md
│   ├── oracle-graalvm.md
│   ├── oracle-jdk.md
│   ├── oracle-linux.md
│   ├── oracle-solaris.md
│   ├── otobo.md
│   ├── ovirt.md
│   ├── pan-cortex-xdr.md
│   ├── pan-gp.md
│   ├── pan-os.md
│   ├── pci-dss.md
│   ├── perl.md
│   ├── phoenix-framework.md
│   ├── php.md
│   ├── phpbb.md
│   ├── phpmyadmin.md
│   ├── pigeonhole.md
│   ├── pixel-watch.md
│   ├── pixel.md
│   ├── plesk.md
│   ├── plone.md
│   ├── pnpm.md
│   ├── podman.md
│   ├── pop-os.md
│   ├── postfix.md
│   ├── postgresql.md
│   ├── postmarketos.md
│   ├── powershell.md
│   ├── privatebin.md
│   ├── proftpd.md
│   ├── prometheus.md
│   ├── protractor.md
│   ├── proxmox-ve.md
│   ├── puppet.md
│   ├── python.md
│   ├── qt.md
│   ├── quarkus-framework.md
│   ├── quasar.md
│   ├── rabbitmq.md
│   ├── rancher.md
│   ├── raspberry-pi.md
│   ├── react-native.md
│   ├── react.md
│   ├── red-hat-ansible-automation-platform.md
│   ├── red-hat-build-of-openjdk.md
│   ├── red-hat-jboss-eap.md
│   ├── red-hat-openshift.md
│   ├── red-hat-satellite.md
│   ├── redis.md
│   ├── redmine.md
│   ├── renovate.md
│   ├── rhel.md
│   ├── robo.md
│   ├── rocket-chat.md
│   ├── rocky-linux.md
│   ├── ros-2.md
│   ├── ros.md
│   ├── roundcube.md
│   ├── routeros.md
│   ├── rtpengine.md
│   ├── ruby-on-rails.md
│   ├── ruby.md
│   ├── rust.md
│   ├── salt.md
│   ├── samsung-galaxy-tab.md
│   ├── samsung-galaxy-watch.md
│   ├── samsung-mobile.md
│   ├── sapmachine.md
│   ├── scala.md
│   ├── sharepoint.md
│   ├── shopware.md
│   ├── silverstripe.md
│   ├── slackware.md
│   ├── sles.md
│   ├── sns-firmware.md
│   ├── sns-hardware.md
│   ├── sns-smc.md
│   ├── solr.md
│   ├── sonarqube-community.md
│   ├── sonarqube-server.md
│   ├── sony-xperia.md
│   ├── sourcegraph.md
│   ├── splunk.md
│   ├── spring-boot.md
│   ├── spring-framework.md
│   ├── sqlite.md
│   ├── squid.md
│   ├── statamic.md
│   ├── steamos.md
│   ├── surface.md
│   ├── suse-linux-micro.md
│   ├── suse-manager.md
│   ├── svelte.md
│   ├── symfony.md
│   ├── tails.md
│   ├── tailwind-css.md
│   ├── tarantool.md
│   ├── tarteaucitron.md
│   ├── telegraf.md
│   ├── teleport.md
│   ├── terraform.md
│   ├── thumbor.md
│   ├── tls.md
│   ├── tomcat.md
│   ├── traefik.md
│   ├── twig.md
│   ├── typo3.md
│   ├── ubuntu.md
│   ├── umbraco.md
│   ├── unity.md
│   ├── unrealircd.md
│   ├── valkey.md
│   ├── varnish.md
│   ├── veeam-backup-and-replication.md
│   ├── veeam-backup-for-microsoft-365.md
│   ├── veeam-one.md
│   ├── virtualbox.md
│   ├── visionos.md
│   ├── visual-cobol.md
│   ├── visual-studio.md
│   ├── vitess.md
│   ├── vmware-cloud-foundation.md
│   ├── vmware-esxi.md
│   ├── vmware-harbor-registry.md
│   ├── vmware-photon.md
│   ├── vmware-srm.md
│   ├── vmware-vcenter.md
│   ├── vue.md
│   ├── vuetify.md
│   ├── wagtail.md
│   ├── watchos.md
│   ├── weakforced.md
│   ├── weechat.md
│   ├── windows-embedded.md
│   ├── windows-nano-server.md
│   ├── windows-powershell.md
│   ├── windows-server-core.md
│   ├── windows-server.md
│   ├── windows.md
│   ├── wireshark.md
│   ├── wordpress.md
│   ├── xcp-ng.md
│   ├── yarn.md
│   ├── yocto.md
│   ├── youtrack.md
│   ├── zabbix.md
│   ├── zentyal.md
│   ├── zerto.md
│   └── zookeeper.md
├── recommendations.md
├── robots.txt
├── runtime.txt
├── schema.html
└── sitemap.xml

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
; This file is for unifying the coding style for different editors and IDEs.
; More information at https://editorconfig.org/

root = true
; Use 2 spaces for indentation in all files

[*]
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
insert_final_newline = true

[*.py]
indent_size = 4


================================================
FILE: .github/FUNDING.yml
================================================
github: endoflife-date
open_collective: endoflife-date


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ""
labels: "enhancement"
assignees: ""
---

**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is.

**Describe the solution you'd like**
A clear and concise description of what you want to happen.

**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.

**Additional context**
Add any other context or screenshots about the feature request here.


================================================
FILE: .github/ISSUE_TEMPLATE/new_product_suggestion.md
================================================
---
name: New product suggestion
about: Suggest a new product for endoflife.date
title: ""
labels: "request"
assignees: ""
---

**Full and short name of product**

Let us know the long, full name and, if it has one, the short name (for example, PowerShell and pwsh).

**Does this product have LTS versions? What are the intervals between each LTS version?**

For example, a current release is a release that occurs between LTS releases. Current releases can contain critical fixes.

**What is the website for the product and for its version information?**

For example: <https://github.com/PowerShell/PowerShell/tags>

**Additional context**

Add any other context or screenshots about the product which you want us to add endoflife.date


================================================
FILE: .github/ISSUE_TEMPLATE/report_incorrect_details.md
================================================
---
name: Report Incorrect Details
about: Report incorrect details of a product on endoflife.date
title: ""
labels: "bug"
assignees: ""
---

**Link to product page on endoflife.date**

Let us know the URL of the product page on endoflife.date you found the incorrect details.

**Details of incorrect and correct details you have found**

For example, the active support date on endoflife.date is not the same as in the product documentation.

endoflife.date: 2021-02-02

Product documentation: 2022-02-02

**What is the source website for the product and for its version information?**

For example: <https://github.com/PowerShell/PowerShell/tags>

**Additional context**

Add any other context or screenshots about the incorrect details.


================================================
FILE: .github/config.yml
================================================
# We use https://github.com/behaviorbot/welcome

# Comment to be posted to on first time issues
newIssueWelcomeComment: >
  Thank you for opening your first issue here :+1:.
  Be sure to follow the issue template if you chose one.

# Comment to be posted to on PRs from first time contributors in your repository
newPRWelcomeComment: >
  Thank you for opening this pull request :+1:.
  If you are not familiar with the project, please check out our
  [Contributing Guidelines](https://endoflife.date/contribute) and our
  [Guiding Principles](https://github.com/endoflife-date/endoflife.date/wiki/Guiding-Principles).
  Also take a look at our [Hacking Guide](https://github.com/endoflife-date/endoflife.date/blob/master/HACKING.md) if you intend to work on site internals.

# Comment to be posted to on pull requests merged by a first time user
firstPRMergeComment: >
  Thank you and congratulations for your first contribution!
  endoflife.date is a community wiki, and we're always looking for more contributions :1st_place_medal: :100: :tada:.


================================================
FILE: .github/copilot-instructions.md
================================================
# endoflife.date Copilot Instructions

This is a Jekyll-based static site that tracks End of Life dates and support lifecycles for various products. The site is built and deployed to Netlify.

## Build, Test, and Lint Commands

### Development Server

```bash
# Install dependencies (first time only)
bundle install

# Run site locally
bundle exec jekyll serve --host localhost --port 4000
# Browse to http://localhost:4000
```

### Building

```bash
# Build the site (output to _site/)
bundle exec jekyll build
```

### Linting

```bash
# Lint a single product file
bin/lint-product.sh products/<product>.md

# Lint all markdown files (run by CI)
npx markdownlint-cli2@latest '**/*.md' '!node_modules' '!vendor'

# Format all files (run by CI)
npx prettier@latest --write .

# Validate formatting (run by CI)
npx prettier@latest --check .
```

### Testing API

```bash
# First tab - run Jekyll
bundle exec jekyll serve

# Second tab - run wiretap for API testing
npx @pb33f/wiretap@latest -s http://localhost:4000/docs/api/v1/openapi.yml -u http://localhost:4000
# Then open http://localhost:9091/ in browser
```

## Architecture

### Product Data Files

- **Product definitions**: Each product is a markdown file in `products/` with YAML frontmatter
- **Schema validation**: Product files validate against `product-schema.json`
- **Categories**: Products are categorized as: `app`, `db`, `device`, `framework`, `lang`, `library`, `os`, `server-app`, `service`, `standard`
- **Data structure**: Each product contains:
  - Metadata (title, permalink, category, tags, icons)
  - Column configuration (which columns to display: eol, eoas, eoes, discontinued, etc.)
  - Custom fields for product-specific data
  - Releases array (sorted newest first)
  - Markdown content after `---` (description and additional info)

### API Generation

- **Plugin-based**: Custom Jekyll plugins in `_plugins/` generate JSON API files during build
- **Main API plugin**: `_plugins/generate-api-v1.rb` creates the `/api/v1/` endpoints
- **OpenAPI spec**: `api_v1/openapi.yml` defines the API schema

### Automation

- **Release data**: Separate repo [`release-data`](https://github.com/endoflife-date/release-data) contains automation scripts
- **Auto-update**: Products with `auto:` configuration get releases automatically updated via:
  - Git tags (GitHub/GitLab repos)
  - Docker Hub
  - npm registry
  - DistroWatch
  - Maven Central
  - Custom scripts
- **CI workflow**: `.github/workflows/auto-merge-release-updates.yml` automatically merges release updates

### Theme and Layout

- **Base theme**: Built on [Just the Docs](https://github.com/just-the-docs/just-the-docs) Jekyll theme
- **Product layout**: `_layouts/product.html` renders individual product pages
- **Partials**: `_includes/` contains reusable components
- **Styling**: `_sass/` contains SASS files

## Key Conventions

### Product Files

1. **Naming**: Filename is `productname.md` (lowercase, dashes for spaces)
2. **Frontmatter only**: Product files are YAML frontmatter with markdown content below
3. **Frontmatter order** (blank line between sections):
   - Product info: `title`, `category`, `tags`, `iconSlug`, `permalink`, `alternate_urls`, `versionCommand`, `releasePolicyLink`, `releaseImage`, `changelogTemplate`
   - Formatting: `releaseLabel`, `LTSLabel`, `eolColumn`, `eoasColumn`, `releaseDateColumn`, `discontinuedColumn`, `eoesColumn`, etc.
   - Identifiers: `identifiers`
   - Auto-update: `auto`
   - Releases: `releases` (each release separated by blank line)
4. **UTC dates**: Use UTC timezone for all dates
5. **Date format**: Use `YYYY-MM-DD` format (unquoted for actual dates - never quote dates)
6. **Version strings**: Always quote version numbers like `"1.2.3"`
7. **Version ranges**: Use space-surrounded dash: `"2 - 5"`
8. **Version lists**: Comma and space separated: `"2, 4 - 7, 9"`
9. **Release cycles**: Use format like `"1.2"` (major.minor), lowercase, no "v" prefix
10. **Release ordering**: Releases must be sorted newest to oldest (each separated by blank line)
11. **Stable only**: Don't add RC/Alpha/Beta/Nightly releases
12. **Boolean dates**: Use boolean `true`/`false` when exact date is unknown
13. **changelogTemplate**: Keep on one line, use double quotes if containing liquid expressions

### Product Content

1. **First paragraph**: Must be a blockquote with product name linked to official site
2. **Description scope**: Keep product description limited to first blockquote only
3. **Line length**: Try to keep at 100 characters maximum
4. **Links**: No link reference definitions except for repeated links
5. **Acronyms**:
   - Explain acronyms if not obvious or part of product name
   - Use `*[ACRONYM]: Full Name` syntax at end of file (not `<abbr>` tags)
   - This avoids repeating definitions
6. **Summary**: Follow with brief release/EOL policy summary
7. **Focus**: Answer key questions readers have:
   - Which versions are supported?
   - Is my version supported?
   - Which version am I running? (via `versionCommand`)
   - How long until I have to upgrade?
   - When is the next release? (if feasible)
   - What does "supported" mean?
8. **Tone and tense**:
   - Use **neutral third-person** voice (avoid "we")
   - Use **present tense** for current policies
   - Use **strong phrasing** (will, is) not weak (could, probably)
   - Example: "We support..." → "Each major version is supported..."
   - Future tense only for actual future changes: "Starting from v23 (due August 2024), each release will be supported..."
   - Once future change is live, revert to present: "Each release is supported..."
9. **Content scope**:
   - Avoid general guidance like installation instructions
   - Some specific helpful guidance is okay (e.g., finding release cycle)
   - Don't mention older policies that only apply to EOL cycles
   - Focus on supported releases
   - Some guesswork is okay for future release/support dates
10. **Supported releases only**: Don't list very old unsupported releases on website (API can include them)
11. **Stable releases only**: Ignore dev, trunk, rc, nightly - only production-ready releases
12. **Primary sources**: Link primarily to official websites, use first-party sources for dates/policies

### URLs and Redirects

1. **Good URLs**: Use obvious, guessable permalinks (e.g., `/nodejs`, `/go`)
2. **Alternate URLs**: Add common variations as redirects (e.g., `/golang` → `/go`, `/node` → `/nodejs`)
3. **No localized URLs**: Avoid URLs with locale codes like `en-us` when linking to docs

### Tags and Icons

1. **Icons**: Use Simple Icons slugs from https://simpleicons.org (set `iconSlug` property)
2. **Tags**: Space-separated, lowercase, singular form, alphabetically ordered
3. **Tag rules**:
   - Use existing tags from https://endoflife.date/tags/
   - Categories are automatically used as tags
   - New tags need discussion via issue first
   - Must be used on 3+ products (except vendor/runtime tags)

### Identifiers

1. **Purpose**: Help SBOM tooling detect products
2. **Types**:
   - `repology: package-name` (shorthand for Repology packages)
   - `purl: pkg:type/name` (Package URL spec)
   - `cpe: cpe:2.3:...` (Common Platform Enumeration)
3. **Avoid duplicates**: Don't add packages already on Repology page

### Custom Fields

1. **Naming**: camelCase
2. **Display locations**: `none`, `api-only`, `after-release-column`, `before-latest-column`, `after-latest-column`
3. **Values**: Always strings, or "N/A" label if missing

### File Organization

- `products/` - Product markdown files (YAML frontmatter + content)
- `_plugins/` - Custom Jekyll plugins (Ruby)
- `_data/` - YAML data files
- `_layouts/` - Page templates
- `_includes/` - Partial templates
- `assets/` - CSS, JS, images
- `api/` and `api_v1/` - API specification files
- `_headers` - Netlify custom headers template
- `_redirects` - Netlify redirects template

## Validation

- Product files auto-validate against JSON schema in IDEs with yaml-language-server
- Add this to product file top for vim:

```yaml
# vim: set ft=yaml :
# yaml-language-server: $schema=../product-schema.json
```

- For VSCode, configure:

```json
"files.associations": {
  "**/products/*.md": "yaml"
},
"yaml.schemas": {
  "../product-schema.json": "products/*.md"
}
```

## Common Tasks

### Breaking Changes

endoflife.date treats certain changes as breaking changes that require special handling to avoid disrupting users and API consumers.

**What counts as a breaking change:**
1. Changing what a product page tracks (switching from X to Y)
2. Splitting or merging product pages
3. Release cycle format changes (e.g., switching from `x.y` to `x`)
4. Definition changes for fields like `lts`, `eol`, or custom columns
5. Page deletions
6. Permalink updates (even with redirects - breaks CORS in API)

**What is NOT a breaking change:**
1. Major changes in the product's actual support policy
2. Product license changes
3. Regular corrections to EOL/release dates (even drastic typos like 2005 → 2025)
4. Compacting old unsupported release cycles
5. Changes in `lts` field usage

**Handling breaking changes (for maintainers):**
1. Tag PR/issue with `Breaking Change` label
2. Announce the change (RSS feed, GitHub issue)
3. Wait minimum 7 days before merging (extend if needed)
4. Add banner to impacted page about upcoming change

### Adding a new product

1. Create `products/productname.md` using template from CONTRIBUTING.md
2. Follow naming conventions above
3. Run `bin/lint-product.sh products/productname.md`
4. Check deploy preview after filing PR

### Updating a release

1. Edit the release entry in the product's `releases:` array
2. Update `latest`, `latestReleaseDate`, and/or date fields
3. Respect the newest-to-oldest ordering

### Adding automation

1. Add `auto:` section with appropriate method (git, docker_hub, npm, etc.)
2. See [Automation wiki](https://github.com/endoflife-date/endoflife.date/wiki/Automation) for details
3. Configure regex and template if needed for version parsing

### Commit Message Format

Follow these conventions for commit messages:

**Format**: `[product-name] Action description (#PR)`

**Examples:**
- `[nodejs] Add 22.0`
- `[python] Update latest for 3.12`
- `[docker-engine] Set EOL for 24.0`
- `[redis] Increase stale release threshold for 7.2`
- `[angular] Update auto configuration`
- `[kotlin] Disable auto-update`
- `[postgresql] Fix formatting`
- `[java] Mark 25 as LTS`

**Product scope prefix:**
- Use lowercase product name in brackets: `[product-name]`
- Match the product filename without `.md`: e.g., `products/amazon-eks.md` → `[amazon-eks]`
- For multi-product changes, omit the prefix or describe the scope

**Action verbs (common patterns):**
- `Add X` - Adding new release cycle or version
- `Update latest` / `Update latest for X` - Updating latest version
- `Set EOL for X` - Setting end-of-life date
- `Mark X as LTS` - Marking release as LTS
- `Fix formatting` / `Fix latest version info` - Corrections
- `Update auto configuration` / `Improve auto configuration` - Auto-update changes
- `Disable auto-update` / `Enable auto-update` - Auto-update toggle
- `Increase/Update stale release threshold` - Threshold adjustments
- `Update links` / `Fix broken links` - Link updates
- `Add identifiers` - Adding PURL/CPE identifiers

**Multi-product changes:**
- `Add per-product event feed`
- `Fix addedAt dates`
- `Remove duplicate identifiers prometheus, harbor`
- `Replace misleading Wikipedia links with explicit references`


================================================
FILE: .github/dependabot.yml
================================================
# To get started with Dependabot version updates, you'll need to specify which
# package ecosystems to update and where the package manifests are located.
# Please see the documentation for all configuration options:
# https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  # This updates the _data/release-data submodule.
  - package-ecosystem: "gitsubmodule"
    directory: "/"
    schedule:
      interval: "cron"
      cronjob: "0 0 * * *" # every day at midnight UTC

  - package-ecosystem: "bundler"
    vendor: true
    directory: "/" # Location of package manifests
    schedule:
      interval: "weekly"

  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "weekly"


================================================
FILE: .github/workflows/auto-merge-release-updates.yml
================================================
name: Dependabot auto-merge release-updates
on: pull_request

# Based on https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions#enable-auto-merge-on-a-pull-request
permissions:
  pull-requests: write
  contents: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - id: metadata
        name: Dependabot metadata
        uses: dependabot/fetch-metadata@21025c705c08248db411dc16f3619e6b5f9ea21a # v2.5.0
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"

      - name: Clone self repository
        uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
        with:
          ref: ${{ github.head_ref }}
          submodules: true

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
        with:
          python-version: "3.12"

      - name: Install Python Dependencies
        run: pip install -r '_data/release-data/requirements.txt'

      - id: latest
        name: Auto-update products
        run: python '_data/release-data/update-product-data.py' -p 'products/'

      - name: Commit changes
        uses: stefanzweifel/git-auto-commit-action@04702edda442b2e678b25b537cec683a1493fcb9 # v5
        with:
          file_pattern: products/*
          commit_message: "🤖: Update latest release data"
          status_options: "--untracked-files=no"
          commit_author: "github-actions[bot] <github-actions[bot]@users.noreply.github.com>"

      - name: Create PR
        if: ${{ contains(steps.metadata.outputs.dependency-names, '_data/release-data') }}
        run: gh pr merge --auto --rebase "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

      - name: Comment on PR about missing releases
        uses: github-actions-up-and-running/pr-comment@f1f8ab2bf00dce6880a369ce08758a60c61d6c0b # v1.0.1
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}
          message: |
            :warning: The following requires some attention:
            ```
            ${{ steps.latest.outputs.warning }}
            ```


================================================
FILE: .github/workflows/check-links.yml
================================================
name: Check URLs

on:
  workflow_dispatch: # Manually run workflow.
  schedule:
    - cron: "0 0 * * 0" # At 00:00 on Sunday.

jobs:
  check_urls:
    runs-on: ubuntu-latest
    # Do not run this job on forks, as it's not cool to query servers for nothing.
    if: ${{ github.repository == 'endoflife-date/endoflife.date' }}
    steps:
      - name: Checkout site
        uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6

      - name: Setup ruby
        uses: ruby/setup-ruby@dffb23f65a78bba8db45d387d5ea1bbd6be3ef18 # v1
        with:
          bundler-cache: true

      - name: Perform URLs check
        run: bundle exec jekyll build
        env:
          MUST_CHECK_URLS: true


================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint

on:
  workflow_dispatch: # Manually run workflow.
  push:
    branches: ["master"]
  pull_request:
    branches: ["master"]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v6

      - uses: actions/setup-node@v6
        with:
          node-version: 20
          cache: "npm"

      - name: Setup ruby
        uses: ruby/setup-ruby@dffb23f65a78bba8db45d387d5ea1bbd6be3ef18 # v1
        with:
          bundler-cache: true

      - name: Build site
        run: bundle exec jekyll build

      - name: Validate OpenAPI specification
        run: npx @quobix/vacuum@latest lint --ignore-file .vacuumignore.yml --details --no-style --no-banner --all-results _site/docs/api/v1/openapi.yml

      - name: Validate Markdown files
        run: npx markdownlint-cli2@latest '**/*.md' '!node_modules' '!vendor'
        continue-on-error: true

      - name: Validate Formatting
        run: npx prettier@latest --check .
        continue-on-error: true


================================================
FILE: .gitignore
================================================
_site
.sass-cache
.jekyll-metadata
vendor/bundle
api/
calendar/
.idea/
.jekyll-cache
_data/gke.json
.bundle/
*.swp
.vscode/*
.history/
*.vsix
.history
.ionide
.vscode/*.code-snippets
*.code-workspace
node_modules
.DS_Store
.venv
wiretap-report.json


================================================
FILE: .gitmodules
================================================
[submodule "_data/release-data"]
	path = _data/release-data
	url = https://github.com/endoflife-date/release-data.git
	branch = main


================================================
FILE: .markdownlint.yaml
================================================
# See https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml.

# Default state for all rules
default: true

# Path to configuration file to extend
extends: null

# MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md003.md
MD003:
  style: "atx"

# MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md004.md
MD004:
  style: "dash"

# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md013.md
MD013:
  line_length: 150

# MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md046.md
MD046:
  style: "fenced"

# MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md049.md
MD049:
  style: "underscore"

# MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md050.md
MD050:
  style: "asterisk"

# MD055/table-pipe-style : Table pipe style : https://github.com/DavidAnson/markdownlint/blob/v0.38.0/doc/md055.md
MD055:
  style: "leading_and_trailing"


================================================
FILE: .prettierignore
================================================
.idea
_includes
_layouts
_sass
_site
api
calendar
manifest.json


================================================
FILE: .prettierrc
================================================
{}


================================================
FILE: .ruby-version
================================================
3.4.6


================================================
FILE: .vacuumignore.yml
================================================
# This file is used to ignore specific linting rules for the OpenAPI specification.

# Ignore ambiguous path warnings for specific paths.
# Those warning are relevants, but it cannot be fixed without breaking changes.
no-ambiguous-paths:
- $.paths['/products/{product}']
- $.paths['/products/full']
- $.paths['/products/{product}/releases/{release}']
- $.paths['/products/{product}/releases/latest']


================================================
FILE: 404.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Page not Found | endoflife.date</title>
  <style>
    body {
      background: #222;
      color: #fff;
      font-family: 'Segoe UI', Arial, sans-serif;
      text-align: center;
      padding: 5em 1em;
    }
    h1 {
      font-size: 5em;
      margin-bottom: 0.2em;
    }
    .emoji {
      font-size: 5em;
      margin-bottom: 0.2em;
    }
    p {
      font-size: 1.5em;
      margin-bottom: 2em;
    }
    a {
      color: rgb(108, 77, 236);
      text-decoration: underline;
      font-weight: bold;
    }
  </style>
</head>
<body>
  <div class="emoji">💀</div>
  <h1>404</h1>
  <p>This page has officially reached its end of life.</p>
  <a href="/">Go to homepage</a>
</body>
</html>


================================================
FILE: CHANGELOG_API.md
================================================
# endoflife.date API Changelog

## API v1.2.0

- Introduce a new `/identifiers/{identifier}` API ([#7361](https://github.com/endoflife-date/endoflife.date/pull/7361))
  to list known Identifiers for given Products. For instance, `/identifiers/purl` will return all known Package URLs
  and the Products they correspond to.

## API v1.1.0

- Expose custom releases field values in the API (#7465). Such fields are grouped under the new `custom`
  field in `ProductRelease`s.

## API v1.0.0

### Summary

API v1 is a major rework of the API v0 with a lot of breaking changes. Compared to the API v0, API
v1:

- feels more _Restful_ (#2431),
- expose almost all product's data (#394, #759, #2062, #2595),
- expose new metadata such as `schema version` (#2331), `total` (for lists), `generated_at` or
  `last modified` date,
- is easier to consume thanks to:
  - new computed fields such as `is_maintained`,
  - the replacement of fields that were using union types with two separate single-type fields:
    - `lts` -> `isLts` and `ltsFrom`,
    - `support` -> `isEoas` and `eoasFrom`,
    - `eol` -> `isEol` and `eolFrom`,
    - `discontinued` -> `isDiscontinued` and `discontinuedFrom`,
    - `extendedSupport` -> `isEoes` and `eoesFrom`.
- provide new endpoints (#2078, #2160, #2530),
- is versioned using the `api/v1` prefix (#2066), making it easier to implement
  non-backward-compatible changes in the future,
- is documented using [swagger-ui](https://github.com/swagger-api/swagger-ui) instead of [Stoplight Elements
  WebComponent](https://github.com/stoplightio/elements/blob/main/docs/getting-started/elements/html.md) (#905),
- but reverts #2425 due to incompatibilities in redirect rules.

The API v1 is now generated using a Jekyll Generator (see <https://jekyllrb.com/docs/plugins/generators/>)
instead of a custom script.

Note that the API v0 is still generated to give time to users to migrate to API v1. It will be
decommissioned at least one year after the API v1 release date.

API v1 documentation can be seen on <https://endoflife.date/docs/api/v1/>.
The old API v0 documentation can still be seen on <https://endoflife.date/docs/api/>.

### Changes in the "All products" endpoint

- Path has been changed from `api/all.json` to `api/v1/products/`
- Response has been changed from a simple array of strings to a JSON document.
  This made it possible to include additional metadata, such as the schema version and the number of
  products.
- Response items has been changed from a simple string (the product name) to a JSON document (#2062).
  This made it possible to include additional information about the product, such as its category
  and tags.
- See <https://endoflife.date/docs/api/v1/#/default/get_products> for a detailed description of the
  response.

### Changes in the "Product" endpoint

- Path has been changed from `api/<product>.json` to `api/v1/products/<product>/`.
- Response has been changed from a simple array of versions to a JSON document.
  This made it possible to include :
  - additional metadata, such as the schema version and the last modified date,
  - product-level information, such as the product label or category (#2062).
- Cycles data now always contain most of the release cycles properties, even if they are null
  (example: `latest`, `latestReleaseDate`).
- See <https://endoflife.date/docs/api/v1/#/default/get_products__product__> for a detailed
  description of the response.

### Changes in the "Cycle" endpoint

- Path has been changed from `api/<product>/<cycle>.json` to `api/v1/products/<product>/cycles/<cycle>/`.
- Response has been changed to make it possible to include additional metadata, such as the schema
  version and the last modified date,
- Cycles data now always contain most of the release cycles properties, even if they are null
  (example: `latest`, `latestReleaseDate`).
- A special `/api/v1/products/<product>/cycles/latest/` cycle, containing the same data as the
  latest cycle, has been added (#2078).
- See <https://endoflife.date/docs/api/v1/#/default/get_products__product__cycles__cycle_> for a
  detailed description of the response.

### Changes in 404 error responses

404 error JSON responses are not returned anymore. #2425 has been reverted because it conflicted
with the rule that rewrites the paths to add `/index.json` to all requests, which is also a global
rule and [takes precedence](https://docs.netlify.com/routing/redirects/#rule-processing-order).

### New endpoints

- `/api/v1/categories`: Get a list of all categories.
- `/api/v1/categories/<category>`: Get a list of all products within the given category.
- `/api/v1/tags`: Get a list of all tags.
- `/api/v1/tags/<tag>`: Get a list of all products having the given tag.
- `/api/v1/products/full`: Get a list of all products with all their details (including cycles).
  This endpoint provides a dump of nearly all the endoflife.date data.

## API v0

On 2025-04-26 the v0 endpoints were:

- "All products" (`/api/all.json`) : Get a list of all product names.
- "Product" (`/api/{product}.json`) : Get all release cycles details for a given product.
- "Cycle" (`/api/{product}/{cycle}.json`) : Get details for a single release cycle of a given product.


================================================
FILE: CODE-OF-CONDUCT.md
================================================
# Contributor Covenant Code of Conduct

## Our Pledge

We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual
identity and orientation.

We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.

## Our Standards

Examples of behavior that contributes to a positive environment for our
community include:

- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
  and learning from the experience
- Focusing on what is best not just for us as individuals, but for the overall
  community

Examples of unacceptable behavior include:

- The use of sexualized language or imagery, and sexual attention or advances of
  any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
  without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
  professional setting

## Enforcement Responsibilities

Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.

Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.

## Scope

This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.

## Enforcement

Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
<endoflife.date-coc@captnemo.in>.
All complaints will be reviewed and investigated promptly and fairly.

All community leaders are obligated to respect the privacy and security of the
reporter of any incident.

## Enforcement Guidelines

Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:

### 1. Correction

**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.

**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.

### 2. Warning

**Community Impact**: A violation through a single incident or series of
actions.

**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or permanent
ban.

### 3. Temporary Ban

**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.

**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.

### 4. Permanent Ban

**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.

**Consequence**: A permanent ban from any sort of public interaction within the
community.

## Attribution

This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
[https://www.contributor-covenant.org/version/2/1/code_of_conduct/][v2.1].

Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][Mozilla CoC].

For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq/][FAQ]. Translations are available at
[https://www.contributor-covenant.org/translations/][translations].

[homepage]: https://www.contributor-covenant.org
[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct/
[Mozilla CoC]: https://github.com/mozilla/diversity
[FAQ]: https://www.contributor-covenant.org/faq/
[translations]: https://www.contributor-covenant.org/translations/


================================================
FILE: CONTRIBUTING.md
================================================
---
layout: page
nav_exclude: true
title: Contributing
description: Some information on how to contribute to https://endoflife.date.
permalink: /contribute
---

- [<img class="emoji" title=":octocat:" alt=":octocat:" src="https://github.githubassets.com/images/icons/emoji/octocat.png" width="20" height="20"> Hacktoberfest](#-hacktoberfest)
- [🕐 What's this project?](#-whats-this-project)
- [✏️ About the codebase](#️-about-the-codebase)
- [➕ Adding a new product](#-adding-a-new-product)
- [✅ Validating your changes](#-validating-your-changes)
- [🆔 Adding Identifiers](#-adding-identifiers)
- [📑 Suggested Reading](#-suggested-reading)
- [⚖️ Code of Conduct](#️-code-of-conduct)

## 🎲 Hacktoberfest

This project is participating in Hacktoberfest.
If you are looking to contribute, please check out our [detailed guide for hacktoberfest participants](https://github.com/endoflife-date/endoflife.date/issues/408).

## 🕐 What's this project?

Before you get started, get to know the project a little bit.
Open [endoflife.date](https://endoflife.date) and browse around a little bit.
Take a look at [some of the recently merged PRs](https://github.com/endoflife-date/endoflife.date/pulls?q=is%3Apr+is%3Aclosed) to get a better idea.

## ✏️ About the codebase

endoflife.date is built using [Jekyll](https://jekyllrb.com/) - the Ruby static site builder that powers GitHub Pages.
The site is built and deployed to [Netlify](https://www.netlify.com/).
Since the site is mostly informational, you _don't need programming skills to contribute to the project_.

## ➕ Adding a new product

To add a new page to the website, [create a new markdown file with YAML frontmatter](https://github.com/endoflife-date/endoflife.date/new/master/products).
Keep the filename as `productname.md`, and please delete any generic comments or unneeded keys before creating a Pull Request.
Use the UTC timezone for all dates, wherever possible.
Below is a template that you can adapt to add a new product:

```yaml
---
# Name of the product (mandatory).
title: Timeturner

# Date when the product was added to endoflife.date (optional).
addedAt: 2019-05-27

# Category of the product (mandatory).
# Possible values are: app, database, device, framework, lang, os, server-app, service, standard.
# If you add a new value, please mention it in the PR description. Some rough guidelines:
# - app: end-user applications
# - database: all kinds of database
# - device: physical, hardware devices
# - framework: application libraries, SDKs, frameworks...
# - lang: programming languages
# - os: operating systems (and similar projects)
# - server-app: applications usually installed on the server-side
# - service: managed service offerings (SaaS/PaaS...)
# - standard: standards and protocols.
category: os

# Tags of the product (optional).
#
# Remember that no tag is better than a useless tag. So do not introduce new tags when adding a product
# and use one of the tags listed on https://endoflife.date/tags/.
#
# Should you want to add a new tag, please open an issue first to discuss it with the team.
# Moreover, any new tag must be applied in a single PR to all products that should have it.
#
# Rules about tags are the following:
# - must match [a-z0-9\-]+,
# - must be declared with a space-separated string,
# - must be alphabetically ordered,
# - must use singular (for example web-server, not web-servers),
# - should not be an existing category (note that categories are automatically used as tags),
# - should be used at least three times, except for tags representing a vendor or a runtime dependency,
# - must be added for one of the following reasons :
#   - set a product family such as linux-distribution, web-browser, mobile-phone or web-server,
#   - set a product vendor such as adobe, amazon or apache,
#   - set a third-party extended support partner, or
#   - set a runtime dependency such as java-runtime, javascript-runtime or php-runtime.
tags: amazon linux-distribution

# Simple Icons icon slug (https://simpleicons.org/) for the product or its vendor (optional).
# Remove this property if no relevant icon is available on Simple Icons.
# As an example, https://simpleicons.org/?q=codemagic links to https://simpleicons.org/icons/codemagic.svg ,
# so the slug is `codemagic` (the SVG filename without extension).
# A list of all slugs is also available on https://github.com/simple-icons/simple-icons/blob/develop/slugs.md .
iconSlug: codemagic

# Main URL for the page (mandatory).
permalink: /timeturner

# Alternate URLs that will redirect to the permalink (optional).
# This is nice to let people use easier-to-remember URLs. For example, we redirect /golang to /go .
alternate_urls:
-   /hourglass

# Command that can be used to check the current product version (optional).
versionCommand: swish and flick

# The more information link (optional).
# If provided, this link is displayed after the product's description.
# This link should contain information about the release policy and schedule. This is NOT the product URL!
# Do not use a localized URL (such as one containing en-us) if possible.
releasePolicyLink: https://nodejs.org/about/releases/

# An image that shows a graphical representation of the releases (optional).
# If provided, this image will be displayed at the top of the product's page.
# This is not the product logo. Remove if you don't find a relevant image.
releaseImage: https://raw.githubusercontent.com/nodejs/Release/main/schedule.svg?sanitize=true

# Template to be used to generate a link for the releases (optional).
# Available variables inside the template are:
# - __RELEASE_CYCLE__: will be replaced by the value of `releaseCycle`,
# - __LATEST__: will be replaced by the value of `latest`,
# - __CODENAME__: will be replaced by the optional `codename`.
# You can even use Liquid Templating inside the template, such as:
#   https://godotengine.org/article/maintenance-release-godot-{{"__LATEST__" | replace:'.','-'}}
# Do not use a localized URL (such as one containing en-us) if possible.
changelogTemplate: "https://link/of/the/__RELEASE_CYCLE__/and/__LATEST__/version"

# Template that generates names for every release (optional, default = "__RELEASE_CYCLE__").
# It supports the same variables as changelogTemplate.
releaseLabel: "MoM Timeturner __RELEASE_CYCLE__ (__CODENAME__)"

# The label that will be used alongside releases labelled with `lts: true`
# (optional, default = "<abbr title='Long Term Support'>LTS</abbr>" ).
# Only provide if the product has LTS releases that are not called LTS, but something else.
# Prefer using an HTML abbr tag, if possible.
LTSLabel: "<abbr title='Extra Long Support'>ELS</abbr>"

# Whether the "End Of Life" column should be displayed (optional, default = true).
# The value of this property can be set to any string to override the default column label.
eolColumn: Security Support

# Whether the "End Of Active Support" column should be displayed (optional, default = false).
# The value of this property can be set to any string to override the default column label.
eoasColumn: Active Support

# Whether the "Latest" column should be displayed (optional, default = true).
# The value of this property can be set to any string to override the default column label.
latestColumn: Latest

# Whether the "Released" column should be displayed (optional, default = true).
# The value of this property can be set to any string to override the default column label.
releaseDateColumn: Released

# Whether the "Discontinued" column should be displayed (optional, default = false).
# Set this to true for physical, hardware devices (as opposed to software projects).
# The value of this property can be set to any string to override the default column label.
discontinuedColumn: Discontinued

# Whether the "End Of Extended Support" column should be displayed (optional, default = false).
# The value of this property can be set to any string to override the default column label.
eoesColumn: Extended Support

# Custom fields configuration (optional).
# Custom fields are non-standard fields used for documenting things such as related runtime versions, custom dates that
# cannot be expressed using the standard fields, etc.
# They can be:
# - displayed in the release table,
# - made available in API responses,
# - used in table includes, such as in https://github.com/endoflife-date/endoflife.date/blob/master/products/ansible.md
#   (preferred this over release table when there are more than 2 or 3 custom fields),
# - or even just used for internal documentation.
# Search in the existing products source file to see how they are used.
customFields:

  # Name of the custom field (mandatory, unique).
  # If the release cycle does not declare this field, the label 'N/A' will be displayed instead.
  # Custom fields follow the camel-case syntax for naming.
  # Values must always be a string.
  - name: supportedIosVersions

    # Where the custom field should be displayed (mandatory). Allowed values are:
    # - none: do not display the custom field in API responses nor in release table.
    # - api-only: only display the custom field in API responses.
    # - after-release-column: display the custom field in API and in the release table after the release column.
    # - before-latest-column: display the custom field in API and in the release table before the latest column.
    # - after-latest-column: display the custom field in API and in the release table after the latest column.
    # If multiple columns have the same position, the order of the column in the customFields list will be respected.
    display: after-release-column

    # Label of the custom field (mandatory).
    # It will notably be used as the column's name in the release table.
    label: iOS

    # A description of what the custom column contains (optional).
    # It will notably be used as the column's tooltip in the release table.
    description: Supported iOS versions

    # A link that gives more information about what the custom field contains (optional).
    # It will notably transform the label into a link in the release table.
    link: https://en.wikipedia.org/wiki/IPhone#Models

# Auto-update release configuration (optional).
# This is used for automatically updating `releaseDate`, `latest`, and `latestReleaseDate` for every release.
# Multiple configurations are allowed.
# Please visit https://github.com/endoflife-date/endoflife.date/wiki/Automation for more details.
# The presence of such configuration modifies the product page so that users are informed that existing
# releases are automatically updated with latest versions.
auto:
  # Mark auto-update as being cumulative (optional, default = false).
  # This means that the data won't be deleted before fetching new data.
  # Activating cumulative updates is not recommended for most products, but could be useful for products that:
  # - have a long history of releases that is long to fetch,
  # - have a history of releases that is not available anymore.
  cumulative: true
  methods:
    # Configuration for auto-update based on git.
    # Any valid git clone URL will work, but support for partialClone is necessary
    # (GitHub and GitLab support it).
    # For example, for Apache Maven:
    - git: https://github.com/apache/maven.git

      # Python-compatible regex that defines how the tags above should translate to versions (optional).
      # The default regex can handle versions having at least 2 digits (ex. 1.2) and at most 4 digits (ex. 1.2.3.4),
      # with an optional leading "v"). Use named capturing groups to capture the version or version's parts.
      # Default value should work for most releases of the form a.b, a.b.c or 'v'a.b.c.
      # It should also skip over any special releases (such as nightly, beta, pre, rc...).
      regex: ^v(?P<major>\d+)_(?P<minor>\d+)_(?P<patch>\d{1,3})_?(?P<tiny>\d+)?$

      # Python-compatible regex that defines which tags should be excluded (optional).
      regex_exclude: ^v99.99.99$

      # A liquid template using the captured variables from the regex above that renders the final version
      # (optional, default can handle versions having a 'major', 'minor', 'patch' and 'tiny' version).
      # You can use liquid templating here.
      template: '{{major}}.{{minor}}.{{patch}}{%if tiny %}p{{tiny}}{%endif%}'

    # Configuration for auto-update based on Docker Hub.
    # The value must be the "owner/repo" combination for a docker hub public image.
    # Use "library" as the owner name for an official docker/community image.
    # For example, for PostgreSQL:
    - docker_hub: library/postgres

    # Configuration for auto-update based on the npm registry.
    # The value must be the package identifier on https://www.npmjs.com .
    # For example, for Vue:
    - npm: vue

    # Configuration for auto-update based on DistroWatch.
    # The value must be the distribution ID. It can be found in the distribution URL.
    # For example, for https://distrowatch.com/index.php?distribution=debian , use "debian".
    - distrowatch: debian

      # The Python-compatible regex used to parse headlines (mandatory).
      # Use named capturing groups to capture the version or version's parts.
      # You can also pass a list of regexes here and matches for any of those will be considered.
      regex: 'Distribution Release: (?P<version>\d+.\d+)'

      # A liquid template using the captured variables from the regex above that renders the final version
      # (optional, default can be found on https://github.com/endoflife-date/release-data/blob/main/src/distrowatch.py#L13 ).
      # You can use liquid templating here.
      template: '{{version}}'

    # Configuration for auto-update based on Maven Central ( https://search.maven.org ).
    # The value must be the maven coordinates of the artifact, in the form groupId/artifactId.
    # For example, for Apache Tomcat ( https://search.maven.org/artifact/org.apache.tomcat/tomcat ):
    - maven: org.apache.tomcat/tomcat

    # Configuration for auto-update based on a custom script in the release-data repository.
    # The value must be the script name in the release-data repository, without it's '.py' extension.
    - custom: script-name

# A list of identifiers that can be used to detect this product as being used,
# especially by SBOM tooling
# Please see https://endoflife.date/help/identifiers-needed/ for more information
identifiers:
  # Each identifier is a way of linking this product to various methods of installing it

  # This is a shorthand to use repology as the source data
  # https://repology.org/project/:package-name-/versions
  # should return a valid list of packages linked to this product.
  - repology: package-name

  # See the PURL spec https://github.com/package-url/purl-spec
  # for details, and avoid packages that are already mentioned on
  # the repology page
  # Common examples would be to use
  # - pkg:os to document operating systems (https://github.com/package-url/purl-spec/pull/161)
  # - pkg:github to link to GitHub pages
  # - pkg:golang/pypi/gem/maven/npm etc for common package managers
  # - pkg:docker for linking to docker images on Docker Hub
  - purl: pkg:package-manager/package-name

# A list of releases, supported or not (mandatory).
# Releases must be sorted from the newest (on top of the list) to the oldest.
# Do not add releases that are not considered "stable" (such as RC/Alpha/Beta/Nightly).
releases:

    # Release cycle name (mandatory, unique, always put in quotes).
    # Only lowercase letters, numbers, dots, dashes, plus and underscores are allowed (/^[a-z0-9.\-_+]+$/).
    # This is usually major.minor. Do not prefix with "v" or suffix with ".x".
-   releaseCycle: "1.2"

    # Name displayed for the release (optional, default = global releaseLabel value).
    # Use this property if you need to override the release label on a per-release basis.
    # You can use templating here, though it is usually not required.
    # Template parameters are the same as the global releaseLabel property.
    releaseLabel: "Timeturner Firebolt (1.2)"

    # Codename of the release (optional, not displayed anywhere by default).
    # It can be used as __CODENAME__ in the releaseLabel and changelogTemplate.
    # It is also returned as-is in the API.
    codename: firebolt

    # Date of the release (mandatory).
    # Note that an approximate date is OK if the exact date is not known.
    releaseDate: 2017-03-12

    # Whether this is a "LTS" release (optional, default = false).
    # What LTS means may differ from product to product (see LTSLabel above).
    # Only provide for a release that will get much longer support than usual.
    # Alternatively, this can be set to a date
    # if the product is not labeled as LTS when it is released (ex. Angular)
    # or when normal versions are promoted LTS after their release (ex. Jenkins).
    lts: true

    # End Of Active Support date (mandatory if eoasColumn is true, else MUST NOT be set).
    # This can be either a date (must be valid and not quoted)
    # or a boolean value (when the date is not known or has not been decided yet).
    # - When a date is used, this is the date where bug fixes stop coming in.
    # - When a boolean is used, it must be set to true if the release cycle is not supported
    #   anymore, and false otherwise.
    eoas: 2018-01-31

    # End Of Life date (mandatory).
    # This can be either a date (must be valid and not quoted)
    # or a boolean value (when the date is not known or has not been decided yet).
    # - When a date is used, this is where all support stops, including security support.
    #   Note that this date reflects what is true for the majority of users (you may use the
    #   eoes field if possible/necessary).
    # - When a boolean is used, it must be set to true if the release cycle is End Of Life,
    #   and false otherwise.
    eol: 2019-01-01

    # End Of Extended/commercial Support date (optional if eoesColumn is true, else SHOULD NOT be set).
    # This can be either a date (must be valid and not quoted),
    # a boolean value (when the date is not known or has not been decided yet), or null.
    # - When a date is used, this is where the extended support period stops.
    # - When a boolean is used, it must be set to true if the extended support period is over,
    #   and false otherwise.
    # - When null is used, it means that there is no extended/commercial support for the given
    #   release cycle.
    eoes: 2020-01-01

    # Discontinuation date (mandatory if discontinuedColumn is true, else MUST NOT be set).
    # This is typically used for physical, hardware devices (as opposed to software projects),
    # to indicate when the device is no longer available for sale or is no longer being manufactured.
    # In contrast, the `eol` property indicates the end of support service for the device version.
    # This can be either a date (must be valid and not quoted)
    # or a boolean value (when the date is not known or has not been decided yet).
    # - When a date is used, this is the date where the release cycle is discontinued.
    # - When a boolean is used, it must be set to true if the release cycle is discontinued,
    #   and false otherwise.
    discontinued: true

    # Latest release for the release cycle (optional if latestColumn is false, else mandatory).
    # Usually this is the release cycle's latest "patch" release.
    # It should be removed if latestColumn is false.
    # Always add quotes around this value.
    latest: "1.2.3"

    # Latest release date (optional).
    # Use valid dates, and do not add quotes around dates.
    latestReleaseDate: 2022-01-23

    # A link to the changelog for the latest release in this cycle
    # (optional, default = the URL generated from changelogTemplate if it is provided).
    # Use this if the link is not predictable (i.e. you can't use changelogTemplate),
    # or if the changelogTemplate generated link must be overridden.
    # Do not use a localized URL (such as one containing en-us) if possible.
    # Use the special value 'null' (unquoted) if you want to disable the link
    # for a specific cycle of a product having a changelogTemplate.
    link: https://example.com/news/2021-12-25/release-1.2.3

# In the following markdown section, ensure that all the above are present:
# 1. A one-line statement about what the product is, with a link to the primary website (in a quote).
# 2. A short summary of the release policy, pointing out the EoL policy as well, if available.
# 3. Any additional information that may be of interest.
#
# See also the Guiding Principles on the wiki ( https://github.com/endoflife-date/endoflife.date/wiki/Guiding-Principles )
# for indication of the tone and voice to use for the text.


# Please leave a new line both above and below the triple-dashes.

---

# All the product information text should be under triple-dashes.
# If you are adding any images in the text, they might get blocked due to our CSP,
# so prefer using releaseImage in such cases.
# Note that images on the same website as releaseImage will not be blocked.

> [Time Turner](https://jkrowling.com/time-turner) is a device that powers short-term time travel.

Time-turners are no longer released, and the last known stable release was in HP.5 release.
```

For the product text, please make sure you read the [Guiding Principles](https://github.com/endoflife-date/endoflife.date/wiki/Guiding-Principles) for the website to match the tone.
File a Pull Request with this file created, and Netlify will provide a preview URL for the same.
Once the pull request is merged, the changes are automatically deployed on the website.
See below for how to validate your changes.

You can visit <https://github.com/endoflife-date/endoflife.date/new/master/products> to directly create your file.

## ✅ Validating your changes

If you're using an IDE like `vscode` or `vim` (or any other IDE that supports JSON schema validation),
you can use [this jsonschema](https://endoflife.date/product-schema.json) to validate the new product.

For `vscode` you need the [yaml-language-server extension](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) and this configuration, which will treat files in the `products` directory as `yaml` files and applies the jsonschema on it:

```json
  "files.associations": {
    "**/products/*.md": "yaml"
  },
  "yaml.schemas": {
    "../product-schema.json": "products/*.md"
  }
```

In `vim` you also could use the [yaml-language-server](https://github.com/redhat-developer/yaml-language-server) and just add the following snippet at the top of the product file:

```yaml
# vim: set ft=yaml :
# yaml-language-server: $schema=../product-schema.json
```

Once you file your Pull Request, Netlify will provide a list of checks for your changes.
If one of the checks fails, you can click Details and see through the errors, or one of the Maintainers will be there to help you.

If all the checks pass, you can click the "Details" link on the "Deploy Preview" Status Check to see a preview of the website _with your changes_.

![image](https://user-images.githubusercontent.com/584253/134535142-7d1170b7-16f4-4cd3-987e-e890b76098d5.png)

Click through, and validate your changes.
Click all the links on the page you've changed and make sure they're not broken.

### Run endoflife.date locally

Please read the [HACKING documentation](https://github.com/endoflife-date/endoflife.date/blob/master/HACKING.md)
for instructions on how to run the endoflife.date locally.

### Testing API payload

There is a GitHub workflow that already validates the OpenAPI specification
(it can also be checked on <https://pb33f.io/doctor/>).
But to test the generated API payload you can do the following:

```sh
# In a first tab, run:
bundle exec jekyll serve

# In a second tab, run:
npx @pb33f/wiretap@latest -s http://localhost:4000/docs/api/v1/openapi.yml -u http://localhost:4000
# then open http://localhost:9091/ in your browser

# In a third tab, run:
IFS="
"
for file in $(find _site/api/v1 -type f | grep -v releases | sort -n); do
  echo $(dirname $file | sed 's|_site|http://localhost:9090|' | sed 's|v1$|v1/|' | sed 's| |%20|')
done | xargs -n1 -P20 curl -s -o /dev/null -w '%{url} %{http_code}\n'
```

### Linting and formatting files

You can use the `bin/lint-product.sh` to lint a product file
using [markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2)
and [prettier](https://github.com/prettier/prettier).

```sh
bin/lint-product.sh products/<product>.md
```

## 🆔 Adding Identifiers

We need help with adding more identifiers.
Please see [this page](/help/identifiers-needed/) for a list of pages missing identifiers.

## 📑 Suggested Reading

We have the following documents which should help you get familiar with the project and the codebase.
You don't need to read all of these, and we've linked these docs above in cases where you must read any of them.

- [HACKING.md](https://github.com/endoflife-date/endoflife.date/blob/master/HACKING.md) contains instructions on setting up the codebase locally with Jekyll. Read this if you're planning to make complex changes or setting up the project locally.
- [Guiding Principles](https://github.com/endoflife-date/endoflife.date/wiki/Guiding-Principles) - These help us make decisions around the content we have. If you'd like to make sure your PR gets speedy approval, please read these to ensure your changes are aligned with the rest of the content. This is _especially important if you are making non-trivial changes_ that deal with the content or add a new product.
- [CONTRIBUTING.md](https://github.com/endoflife-date/endoflife.date/blob/master/CONTRIBUTING.md) - (This page). Also accessible at <https://endoflife.date/contribute>

## ⚖️ Code of Conduct

Please note that this project is released with a [Contributor Code of Conduct](https://github.com/endoflife-date/endoflife.date/blob/master/CODE-OF-CONDUCT.md).
By participating in this project you agree to abide by its terms.


================================================
FILE: Gemfile
================================================
source "https://rubygems.org"

gem "jekyll", "~> 4.4.1"

# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
# gem "github-pages", group: :jekyll_plugins

# If you have any plugins, put them here!
group :jekyll_plugins do
  gem 'jekyll-feed', '~> 0.17'
  gem 'jekyll-timeago'
  gem 'just-the-docs', '~> 0.12.0'
  gem 'jekyll-seo-tag'
  gem 'jekyll-last-modified-at'
  gem 'jemoji'
end

# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem "tzinfo-data", platforms: [:mingw, :mswin, :x64_mingw, :jruby]

# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.2.0" if Gem.win_platform?

gem "webrick", "~> 1.9"

gem 'icalendar', '~> 2.12'

# Used in product-data-validator to check URLs
gem "open-uri", "~> 0.5"

# Used in purl-to-url to parse PURLs
gem "packageurl-ruby", "~> 0.2.0"


================================================
FILE: HACKING.md
================================================
- [Development](#development)
- [Build](#build)
- [File and Directory structure](#file-and-directory-structure)
- [Automation](#automation)
- [API](#api)
  - [API Documentation](#api-documentation)
- [Contributing Workflow](#contributing-workflow)
- [Deployment](#deployment)
- [Analytics](#analytics)
- [Automation](#automation)

endoflife.date uses [Jekyll](https://jekyllrb.com/), the static website generator.
This document helps you set the codebase locally.
This isn't necessary for most content changes.
Follow this guide if you are making layout, design, or code changes.

## Development

First, you will need to install Ruby and Bundler.
Follow [these instructions](https://www.ruby-lang.org/en/documentation/installation/) to install Ruby, and then run the following commands:

```sh
# Install bundler
gem install bundler

# Clone the project:
git clone --recurse-submodules git@github.com:endoflife-date/endoflife.date.git
cd endoflife.date

# Install dependencies (_Note: You must use Bundler 2 or greater_):
$ bundle install

# All of the following commands should run successfully at this point:
ruby --version
bundle --version
bundle exec jekyll --version
```

## Build

Run the site locally:

```sh
bundle exec jekyll serve --host localhost --port 4000
```

Browse to `http://localhost:4000` and you should see the site running locally.
If you find any errors at this stage, check [Jekyll's troubleshooting page](https://jekyllrb.com/docs/troubleshooting/#configuration-problems)
or [ask a question in the Q&A category](https://github.com/endoflife-date/endoflife.date/discussions/new/) on GitHub Discussions.

Other Jekyll commands [are documented](https://jekyllrb.com/docs/usage/) on the Jekyll website,
along with the command options for the [build](https://jekyllrb.com/docs/configuration/options/#build-command-options) and [serve](https://jekyllrb.com/docs/configuration/options/#serve-command-options) commands.

## File and Directory structure

- The layout for the products page is in `_layouts/product.html`
- Product data is in the `products` directory.
- Automation scripts that updates latest releases are in the [`release-data`](https://github.com/endoflife-date/release-data/) repository.
  There are also some information in the [Automation](https://github.com/endoflife-date/endoflife.date/wiki/Automation) page on the wiki.
- We follow the following directory structure:
  - `_includes` holds partial templates, such as the content for the `<head>` tag.
  - `assets` includes CSS/JS/Logo images...
  - `_plugins` holds scripts invoked by the Jekyll build code.
  - `_config.yml` holds the Jekyll configuration, including list of plugins, exclude/include filelist, theme configuration, and plugin settings.
  - `Gemfile` and `Gemfile.lock` are package files for bundler.
- `_headers` holds the template for generating a list of custom HTTP headers, in the Netlify Headers Format.
  A rendered version of the file can be seen in the `_site/_headers` after building the site.
- `_redirects` holds the template for generating redirects from alternate URLs to main product pages, again in the Netlify format.
  A rendered version of the file can be seen in the `_site/_redirects` after building the site.
- [`robots.txt`](https://en.wikipedia.org/wiki/Robots.txt) is for web scraping robots.
- [`humans.txt`](https://endoflife.date/humans.txt) holds details about the people and tech behind the project.

## Extending the Jekyll theme

The site is based on the [Just the Docs](https://github.com/just-the-docs/just-the-docs) Jekyll theme.
Take a look at [the documentation](https://just-the-docs.github.io/just-the-docs/) for knowing more about its configuration.
Beware, this configuration is for the current `main` branch, not for the version used by this site.

If you need to override some parts, take a look at [the customization section](https://just-the-docs.github.io/just-the-docs/docs/customization/) of the documentation.

## Logo

The [site logo](/assets/logo.svg) is an adaptation of [An hourglass in a round icon](https://commons.wikimedia.org/wiki/File:Hourglass_icon_%28orange%29.svg) by David Abián and Serhio Magpie.
The logo is representing the concepts of time (with the hourglass) and EOL/cycles (with the colored split circle).

Derived icons for various usages, such as [the web app manifest](/manifest.json) were generated using [RealFaviconGenerator.net](https://realfavicongenerator.net/).

All icons are placed in the [`assets`](/assets) directory.
Our theme tries to pick the favicon from `/favicon.ico`, which we don't have to avoid this behaviour.
However, many browsers will [assume this location anyway](https://stackoverflow.com/a/21359390/374236),
so we have a redirect from /favicon.ico to a PNG version instead.

Note that `android-chrome-*.png` icons were renamed to `logo-*.png`.
Those icons are used in other contexts, such as on the site as a logo.

## API

The API is just JSON files generated by the `_plugins/generate-api-v1.rb` custom plugin.

### API Documentation

The current API v1 documentation is available at <https://endoflife.date/docs/api/v1/>
and is generated from an OpenAPI Specification file located at `api_v1/openapi.yml`.
The documentation is rendered by [Swagger UI](https://swagger.io/tools/swagger-ui/).

The old API v0 documentation is still available at <https://endoflife.date/docs/api>
and is generated from an OpenAPI Specification file located at `assets/openapi.yml`.
The documentation is rendered by [Stoplight Elements](https://meta.stoplight.io/docs/elements/ZG9jOjMyNjU4OTY0-introduction-to-elements).

## Contributing Workflow

If you just want to add a new product or make some trivial changes, please see [`CONTRIBUTING.md`](https://github.com/endoflife-date/endoflife.date/blob/master/CONTRIBUTING.md).

Else:

- Fork the project,
- Create your feature branch (git checkout -b my-new-feature),
- Commit your changes (git commit -am 'Add some feature'),
- Push to the branch (git push origin my-new-feature),
- Create new Pull Request.

## Deployment

The code is built and deployed to Netlify under its Open Source Plan.
We use the following Netlify Features:

- custom HTTP Headers (`_headers` file),
- custom Redirects (`_redirects` file),
- easy deploy previews,
- future plans to use Netlify Functions.

The build script is kept in `netlify.toml`.

## Analytics

There are no javascript trackers or analytics on the website.

Numbers from Google Search Reports are published on [the wiki](https://github.com/endoflife-date/endoflife.date/wiki/Google-Search-Usability-Reports).
The data provided by Google is for publishers, and is based on search queries that showed endoflife.date in the search results.
Google has more details [here](https://support.google.com/webmasters/answer/96568), including limitations of this data.
Rare queries are omitted by Google from this data to protect user privacy.

Numbers from Netlify Analytics are published on [the wiki](https://github.com/endoflife-date/endoflife.date/wiki/Netlify-Analytics-Reports).

## Automation

The endoflife.date project runs a bit of automation on top of GitHub Actions to automate mundane tasks.

Automation is currently focused towards updating release data using [the `release-data` repository](https://github.com/endoflife-date/release-data).

This is documented in the [wiki](https://github.com/endoflife-date/endoflife.date/wiki/Automation).


================================================
FILE: LICENSE
================================================
Copyright 2020 endoflife.date contributors

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
# endoflife.date

[![Netlify Status](https://api.netlify.com/api/v1/badges/92f7a2a9-3cca-4916-a75e-f9db4ec39d48/deploy-status)](https://app.netlify.com/sites/endoflife-date/deploys)
[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://opensource.guide/how-to-contribute/#opening-a-pull-request)
[![powered by Jekyll](https://img.shields.io/badge/powered_by-Jekyll-blue.svg)](https://jekyllrb.com/)
[![Website shields.io](https://img.shields.io/website-up-down-green-red/https/endoflife.date.svg)](https://endoflife.date/)
[![made-with-Markdown](https://img.shields.io/badge/Made%20with-Markdown-1f425f.svg)](https://commonmark.org/)
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE-OF-CONDUCT.md)
[![Gitter](https://badges.gitter.im/endoflife-date/community.svg)](https://gitter.im/endoflife-date/community)
[![Twitter Follow Badge](https://img.shields.io/twitter/url.svg?label=@endoflife_date&style=social&url=https%3A%2F%2Ftwitter.com%2Fendoflife_date)](https://twitter.com/endoflife_date)

Keep track of various End of Life dates and support lifecycles for various products.
Visit <https://endoflife.date> for a list of supported products.
This information is very often [hard to track or badly presented](https://twitter.com/captn3m0/status/1110504412064239617).
This project collates this data and presents it in an easily accessible format, with URLs that are
easy to guess and remember.

If you maintain release information (end-of-life dates, or support information) for a product,
we have a [set of recommendations](https://endoflife.date/recommendations) along with a checklist on
some best practices for publishing this information.

## Contributing

Please see [the contributing guide](https://endoflife.date/contribute) for details.
While participating in the project, you must abide by its [Code of Conduct](CODE-OF-CONDUCT.md).

## API

An API is available for integration with CI platforms. API documentation is available at <https://endoflife.date/docs/api/v1/>.
The API is currently in Beta, and breaking changes can happen.

## License

Licensed under the [MIT License](LICENSE).

## Credits

endoflife.date is relying on various amazing software and components :

- [GitHub](https://github.com/), an Internet hosting service for software development and version control.
- [Jekyll](https://jekyllrb.com/), a static site generator.
- [Ruby](https://www.ruby-lang.org/), a dynamic and open source programming language with a focus on simplicity and productivity.
- [Just the Docs](https://github.com/just-the-docs/just-the-docs), a documentation theme for Jekyll.
- [Swagger UI](https://swagger.io/tools/swagger-ui/), a documentation generator for OpenAPI Specification.
- [Simple Icons](https://simpleicons.org/), free SVG icons for popular brands.
- [Tabler Icons](https://github.com/tabler/tabler-icons), a complete icon set with perfect line weights and spacing - ready for Figma, apps, and design systems.
- Our icon is derived from [Hourglass icon (orange)](https://commons.wikimedia.org/wiki/File:Hourglass_icon_%28orange%29.svg) by David Abián and Serhio Magpie on the English Wikipedia, remixed under the CC-BY-SA-4.0 license.
- [RealFaviconGenerator](https://realfavicongenerator.net/), a favicon Generator, for real.
- [Netlify](https://www.netlify.com/), an all-in-one platform for automating modern web projects.
- Product descriptions are adapted from the [English Wikipedia](https://en.wikipedia.org/), under [CC BY-SA 3.0](https://en.wikipedia.org/wiki/Wikipedia:Text_of_Creative_Commons_Attribution-ShareAlike_3.0_Unported_License) license.


================================================
FILE: _config.yml
================================================
---
# Jekyll configuration - https://jekyllrb.com/docs/configuration/

# Site settings
url: https://endoflife.date
title: endoflife.date

# Build settings
encoding: utf-8
markdown: kramdown
strict_front_matter: true # Cause a build to fail if there is a YAML syntax error (#40).
plugins:
  - jekyll-feed
  - jekyll-timeago
  - jekyll-seo-tag
  - jekyll-last-modified-at
  - jemoji

# Silence Saas deprecation warnings, to be removed after this is fixed in just-the-docs
sass:
  quiet_deps: true # https://github.com/just-the-docs/just-the-docs/issues/1541
  silence_deprecations: ["import"] # https://github.com/just-the-docs/just-the-docs/issues/1607

# just-the-docs settings, see https://just-the-docs.com/
theme: just-the-docs
nav_sort: case_insensitive

# https://just-the-docs.com/docs/configuration/#search
search_enabled: true
search:
  button: true
  focus_shortcut_key: "k"
  placeholder_text: "Search for a product"

# https://just-the-docs.com/docs/configuration/#aux-links
aux_links:
  Recommendations:
    - /recommendations
  Contribute:
    - /contribute
  Source:
    - https://github.com/endoflife-date/endoflife.date
  API:
    - /docs/api/v1/
  "Release Data":
    - https://github.com/endoflife-date/release-data/

# https://just-the-docs.com/docs/configuration/#callouts
callouts:
  warning:
    title: Warning
    color: yellow
  note:
    title: Note
    color: blue
  commandInfo:
    title: To learn the current version on your system
    color: grey-dk

# jekyll-timeago plugin configuration, see https://github.com/markets/jekyll-timeago
jekyll_timeago:
  # Use 2 terms in relative timestamps:
  # [YES] x years, y months
  # [YES] x months, z weeks
  # [NO] x years, y months, z days
  depth: 2
  # Give approx times in relative time
  # within a 10% error margin
  # See https://github.com/markets/jekyll-timeago/pull/24
  # for what this does
  threshold: 0.1

# Default pages / products values
defaults:
  - scope:
      path: ""
    values:
      image: /assets/logo-512x512.png
  - scope:
      path: "products"
    values:
      layout: product
      alternate_urls: []
      identifiers: []
      auto: []
      latestColumn: true
      latestColumnLabel: "Latest"
      releaseDateColumn: true
      releaseDateColumnLabel: "Released"
      discontinuedColumn: false
      discontinuedColumnLabel: "Discontinued"
      eoasColumn: false
      eoasColumnLabel: "Active Support"
      eolColumn: true
      eolColumnLabel: "Security Support"
      eoesColumn: false
      eoesColumnLabel: "Extended Support"
      staleReleaseThresholdDays: 365
      customFields: []
      LTSLabel: '<abbr title="Long Term Support">LTS</abbr>'

# Include & excludes
include:
  - _redirects
  - _headers

exclude:
  - .idea
  - bin
  - CODE-OF-CONDUCT.md
  - Gemfile
  - Gemfile.lock
  - HACKING.md
  - LICENSE
  - netlify.toml
  - node_modules
  - package.json
  - package-lock.json
  - vendor/
  - Rakefile
  - README.md
  - requirements.txt
  - runtime.txt
  - wiretap-report.json


================================================
FILE: _headers
================================================
---
# Netlify _headers template. See syntax on https://docs.netlify.com/routing/headers/.
#
# This configuration sets default headers for pages and API responses, and:
# - a CSP and a "Link" header pointing to the API for product pages,
# - a more restrictive CSP for non-product pages,
# - a CSP that allows Stoplight Elements to load correctly for /docs/api.
#
# For a rationale of all the CSP headers, see https://github.com/endoflife-date/endoflife.date/wiki/CSP-Headers.

# Setting a layout forces Jekyll to render this file.
layout: null
---
# Default headers for all pages.
/*
  X-Frame-Options: DENY
  X-XSS-Protection: 1; mode=block
  # Generated using https://www.permissionspolicy.com/
  Permissions-Policy: accelerometer=(), ambient-light-sensor=(), autoplay=(), battery=(), camera=(), cross-origin-isolated=(), display-capture=(), document-domain=(), encrypted-media=(), execution-while-not-rendered=(), execution-while-out-of-viewport=(), fullscreen=(), geolocation=(), gyroscope=(), keyboard-map=(), magnetometer=(), microphone=(), midi=(), navigation-override=(), payment=(), picture-in-picture=(), publickey-credentials-get=(), screen-wake-lock=(), sync-xhr=(), usb=(), web-share=(), xr-spatial-tracking=(), clipboard-read=(), clipboard-write=(), gamepad=(), speaker-selection=(), conversion-measurement=(), focus-without-user-activation=(), hid=(), idle-detection=(), interest-cohort=(), serial=(), sync-script=(), trust-token-redemption=(), unload=(), window-placement=(), vertical-scroll=()
  X-Content-Type-Options: nosniff
  Referrer-Policy: strict-origin
  Strict-Transport-Security: max-age=31536000; includeSubDomains; preload

# Default headers for static resources
/assets/*
  Cache-Control : public, max-age=3600;
/browserconfig.xml
  Cache-Control : public, max-age=3600;
/favicon.ico
  Cache-Control : public, max-age=3600;
/manifest.json
  Cache-Control : public, max-age=3600;

# Default headers for API resources.
/api*
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET
  Access-Control-Max-Age: 86400

# Default headers for calendar resources.
/calendar/*
  Access-Control-Allow-Origin: *
  Access-Control-Allow-Methods: GET
  Access-Control-Max-Age: 86400

# Configuration for all "pages" in the website (e.g. product pages, website pages such as / or /recommendations, API "pages"...).
{% assign defaultCspImgSrc="'self' https://img.shields.io https://www.netlify.com https://cdn.jsdelivr.net/ https://github.githubassets.com/ https://user-images.githubusercontent.com/ https://github-production-user-asset-6210df.s3.amazonaws.com" %}
{%- for page in site.html_pages -%}
{{page.url}}
  {%- if page.layout == 'product' %}
    {%- if page.releaseImage %}
      {% capture releaseImageSrc %}https://{{ page.releaseImage | parse_uri: 'host' }}{% endcapture %}
    {% else %}
      {% assign releaseImageSrc="" %}
    {% endif %}
    Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self'; img-src {{ defaultCspImgSrc }} {{ releaseImageSrc }}
    Link: /api{{page.permalink}}.json; rel=alternate;type=application/json
    Link: /calendar{{page.permalink}}.ics; rel=alternate;type=text/calendar
  {% elsif page.permalink contains '/docs/api/v' %}
    {%- comment %}Used contains to match all API version (startswith does not exist){% endcomment %}
    # unsafe-inline and data: should not be an issue for a static site
    Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self' 'unsafe-inline' https://unpkg.com/; style-src 'self' https://unpkg.com/; img-src 'self' data:
  {% elsif page.permalink == '/docs/api' %}
    Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self' https://unpkg.com/@stoplight/elements/web-components.min.js; style-src 'self' https://unpkg.com/@stoplight/elements/ 'unsafe-inline'
{% else %}
  Content-Security-Policy: default-src 'none'; manifest-src 'self'; connect-src 'self'; script-src 'self' 'sha256-D2OQPa3wGsiYErb6pJgPDNC606ggwvXfh9vbd7X1aks='; style-src 'self'; img-src {{ defaultCspImgSrc }}
{% endif %}


{% endfor %}


================================================
FILE: _includes/css/activation.scss.liquid
================================================
/*
 * In Just the Docs v0.7.0, overriding _includes/css/activation.scss.liquid with an empty file
 * results in a background image on all the links in the main navigation panel when JS is disabled.
 * This suppress those images. Note that those rules are ignored when JS is enabled.
 * See https://github.com/just-the-docs/just-the-docs/pull/1358#issuecomment-1787487607.
 */
.site-nav ul li a {
  background-image: none;
}


================================================
FILE: _includes/custom-column-td.html
================================================
{%- comment %}
Render a product custom column data cell (<td>).

Parameters:
- release (mandatory): a product release cycle definition.
- column (mandatory): the custom column definition.
- cssClasses (optional): a space-separated list of CSS classes to add to the cell.
{% endcomment %}
{%- assign release = include.release %}
{%- assign name = include.column.name %}
{%- assign cssClasses = include.cssClasses | default:'' %}
<td class="{{ cssClasses }}">{{ release[name] | default: 'N/A' }}</td>


================================================
FILE: _includes/custom-column-th.html
================================================
{%- comment %}
Render a product custom column header cell (<th>).

Parameters:
- column (mandatory): the custom column definition.
{% endcomment %}
{%- assign label = include.column.label %}
{%- assign description = include.column.description %}
{%- assign link = include.column.link %}
<th title="{{ description }}">
  {% if link %}<a href="{{ link }}">{{ label }}</a>{% else %}{{ label }}{% endif %}
</th>


================================================
FILE: _includes/head_custom.html
================================================
{%- comment %}
Favicons were generated from the SVG icons with https://realfavicongenerator.net.

The SVG favicon supports dark mode (https://blog.tomayac.com/2019/09/21/prefers-color-scheme-in-svg-favicons-for-dark-mode-icons/).
{% endcomment %}
<link rel="alternate" type="application/atom+xml" title="New products feed" href="/new-products.atom" />
{% if page.layout == "product" %}
<link rel="alternate" type="text/calendar" title="{{ page.title }} events calendar" href="webcal://{{site.url | split: '://' | last}}/calendar{{page.permalink}}.ics" />
<link rel="alternate" type="application/atom+xml" title="{{ page.title }} events feed" href="{{ page.permalink | relative_url }}.atom" />
{% endif %}
<link rel="apple-touch-icon" sizes="180x180" href="{{ '/assets/apple-touch-icon.png' | relative_url }}">
<link rel="icon" type="image/svg+xml" href="{{ '/assets/favicon.svg' | relative_url }}">
<link rel="icon alternate" type="image/png" sizes="32x32" href="{{ '/assets/favicon-32x32.png' | relative_url }}">
<link rel="icon alternate" type="image/png" sizes="16x16" href="{{ '/assets/favicon-16x16.png' | relative_url }}">

<link rel="manifest" href="{{ 'manifest.json' | relative_url }}">
<link rel="mask-icon" href="{{ '/assets/safari-pinned-tab.svg' | relative_url }}" color="#5bbad5">
<meta name="msapplication-TileColor" content="#da532c">
<meta name="msapplication-config" content="{{ '/browserconfig.xml' | relative_url }}">
<meta name="theme-color" content="#ffffff">


================================================
FILE: _includes/identifiers.html
================================================
{% if page.identifiers.size > 0 %}
<details>
  <summary>Show Product Identifiers</summary>
  <ul>
  {% for identifier_hash in page.identifiers %}
    {% assign type_and_identifier = identifier_hash|first %}
    {% assign identifier_type = type_and_identifier[0] %}
    {% assign identifier = type_and_identifier[1] %}
    {% assign identifier_url = identifier_hash.url %}

    <li>
      {{ identifier_type }}:
      {%- if identifier_url %}
      <a href="{{ identifier_url }}"><code>{{ identifier }}</code></a>
      {%- else %}
      <code>{{ identifier }}</code>
      {%- endif %}
    </li>
  {% endfor %}
  </ul>
</details>
{% endif %}


================================================
FILE: _includes/lunr/custom-data.json
================================================
{%- capture newline %}
{% endcapture -%}
{%- assign identifiers = "" %}
{% for identifier_hash in include.page.identifiers %}
{% assign type_and_identifier = identifier_hash | first %}
{% assign identifiers = identifiers | append: " " | append: type_and_identifier[1] %}
{% endfor -%}
"category": {{ include.page.category | markdownify | replace:newline,' ' | strip_html | normalize_whitespace | strip | jsonify }},
"tags": {{ include.page.tags | markdownify | replace:newline,' ' | strip_html | normalize_whitespace | strip | jsonify }},
"iconSlug": {{ include.page.search_terms | markdownify | replace:newline,' ' | strip_html | normalize_whitespace | strip | jsonify }},
"alternate_urls": {{ include.page.alternate_urls | markdownify | replace:newline,' ' | strip_html | normalize_whitespace | strip | jsonify }},
"identifiers": {{  identifiers | markdownify | replace:newline,' ' | strip_html | normalize_whitespace | strip | jsonify }},


================================================
FILE: _includes/lunr/custom-index.js
================================================
const content_to_merge = [
  docs[i].content,
  docs[i].category,
  docs[i].tags,
  docs[i].alternate_urls,
  docs[i].iconSlug,
  docs[i].identifier,
];
docs[i].content = content_to_merge.join(' ');


================================================
FILE: _includes/nav_footer_custom.html
================================================
<a href="https://github.com/endoflife-date/endoflife.date/#credits">Credits</a>


================================================
FILE: _includes/product-icon.html
================================================
{%- assign product_icon_url = include.product.iconUrl %}
{%- assign product_icon_category = include.product.category %}
{%- assign product_icon_description = include.product.title %}
{%- assign product_icon_size = include.size %}
{%- unless product_icon_url %}
  {%- assign product_icon_url = '/assets/category-' | append: product_icon_category | append: '.svg' | relative_url %}
  {%- assign product_icon_description = 'Icon for ' | append: product_icon_category %}
{%- endunless %}
<img class="product-logo" width="{{ product_icon_size }}" src="{{ product_icon_url }}" alt="{{ product_icon_description }} logo">


================================================
FILE: _includes/table.html
================================================
{%- comment %}
Render a table from the given rows.

Considering a table with N column and M rows, the equivalent Markdown table will be :

| labels[0]          | labels[1]          | ... | labels[N]          |
|--------------------|--------------------|-----|--------------------|
| rows[0][fields[0]] | rows[0][fields[1]] | ... | rows[0][fields[N]] |
| rows[1][fields[0]] | rows[1][fields[1]] | ... | rows[1][fields[N]] |
| ...                | ...                | ... | ...                |
| rows[M][fields[0]] | rows[M][fields[1]] | ... | rows[M][fields[N]] |

Parameters:
- rows: Rows used to build the table.
- fields: A comma-separated list of row field names.
- labels: A comma-separated list of column labels.
          The size of the list must be identical to the fields list size.
- types: A comma-separated list of column types.
         The size of the list must be identical to the fields list size.
         Available type are :
         - raw: display the value "as is". The raw type is also used when type is unknown.
         - date: display the value using the date_to_string filter,
                 see https://jekyllrb.com/docs/liquid/filters/#date-to-string.
         - timeago: display the value using the timeago filter,
                 see https://github.com/markets/jekyll-timeago.
         - end-date: display the value as an end of something date (such as support or EOL).
                 This is the "classic" way do display end of support or EOL date on endoflife.date, with:
                 - a background color as a visual indication,
                 - the value displayed using both the date_to_string and timeago filters,
                 - a support for both boolean and date values.
{% endcomment %}
{%- assign labels = include.labels | split:',' %}
{%- assign fields = include.fields | split:',' %}
{%- assign types = include.types | split:',' %}
{%- assign rows = include.rows %}
<table>
  <thead>
    <tr>
      {%- for label in labels %}<th>{{ label }}</th>{% endfor %}
    </tr>
  </thead>
  <tbody>
{%- for row in rows %}
    <tr>
  {%- for field in fields %}
    {%- assign type = types[forloop.index0] %}
    {%- assign value = row[field] %}
    {%- if type == "date" %}
      <td>{{ value | date_to_string }}</td>
    {%- elsif type == "timeago" %}
      <td>{{ value | timeago }}</td>
    {%- elsif type == "end-date" %}
      <td class="{{ row[field] | end_color }}">
      {%- if value == true %}}
        Yes
      {%- elsif value == false %}
        No
      {%- else %}
        {{ row[field] | date_to_string }}
        <div>({{ row[field] | timeago }})</div>
      {%- endif %}
      </td>
    {%- else %}
      <td>{{ row[field] }}</td>
    {%- endif %}
  {%- endfor %}
    </tr>
{%- endfor %}
  </tbody>
</table>


================================================
FILE: _includes/variables.html
================================================
{%- assign api = site.data.openapi -%}

{%- capture title -%}
  {%- if page.title -%}
    {{- page.title | append: ' - ' -}}
  {%- endif -%}
  {{- api.info.title -}}
{%- endcapture -%}

{%- assign description = api.info.description | xml_escape -%}

{%- assign url = page.url | prepend: site.baseurl | prepend: site.url -%}

{%- assign image = '/assets/img/image.png' | prepend: site.baseurl | prepend: site.url -%}

{%- assign collections = '' -%}
{%- for path in api.paths -%}
  {%- assign path_parts = path[0] | split: '/' -%}
  {%- assign collections = collections | append: path_parts[1] | append: ',' -%}
{%- endfor -%}
{%- assign collections = collections | split: ',' | uniq -%}

================================================
FILE: _layouts/json.json
================================================
{{ page.data | jsonify }}


================================================
FILE: _layouts/new-products-feed.atom
================================================
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>{{ site.url }}/new-products.atom</id>
  <title>endoflife.date: new products</title>
  <subtitle>Products recently added to endoflife.date</subtitle>
  <link href="{{ '/new-products.atom' | absolute_url }}" rel="self" />
  <link href="{{ site.url }}"/>
  <updated>{{ page.products.first.added_at | date_to_xmlschema }}</updated>
  <author><name>endoflife.date</name></author>
{%- for product in page.products %}
  <entry>
    <id>{{ product.link }}</id>
    <link href="{{ product.link }}"/>
    <updated>{{ product.added_at | date_to_xmlschema }}</updated>
    <title>{{ product.title | xml_escape }} added</title>
    <summary>{{ product.title | xml_escape }} has been added to endoflife.date.</summary>
  </entry>
{% endfor -%}
</feed>


================================================
FILE: _layouts/page.html
================================================
---
layout: default
---

{{content}}

<script>
// Automatically focus the #search-input element once it appears in the DOM.
// Fixes #367 can be removed whenever upstream/just-the-docs brings a better solution
document.addEventListener('DOMContentLoaded', function() {
  const observer = new MutationObserver((mutations, obs) => {
    const searchInput = document.getElementById('search-input');
    if (searchInput) {
      searchInput.focus();
      obs.disconnect();
    }
  });

  observer.observe(document.body, { childList: true, subtree: true });
});
</script>


================================================
FILE: _layouts/product-feed.atom
================================================
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>{{ page.product_link }}</id>
  <title>endoflife.date: {{ page.product_label | xml_escape }} events</title>
  <subtitle>{{ page.product_label | xml_escape }} release and end-of-life events</subtitle>
  <link href="{{ page.url | absolute_url }}" rel="self" />
  <link href="{{ page.product_link }}"/>
  <updated>{{ page.last_updated | date_to_xmlschema }}</updated>
  <author><name>endoflife.date</name></author>
{%- assign sorted_events = page.events | sort: "occurred_at" -%}
{%- for event in sorted_events %}
  <entry>
    <id>{{ page.product_link }}/{{ event.release_name }}/{{ event.type }}</id>
    <link href="{{ page.product_link }}"/>
    <updated>{{ event.occurred_at | date_to_xmlschema }}</updated>
{%- if event.type == "release" %}
    <title>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} released</title>
    <summary>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} has been released.</summary>
{% elsif event.type == "eoas" %}
    <title>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} end of active support</title>
    <summary>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} active support ended.</summary>
{% elsif event.type == "eol-7d" %}
    <title>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} upcoming end of life</title>
    <summary>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} will be end-of-life in 7 days.</summary>
{% elsif event.type == "eol" %}
    <title>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} end of life</title>
    <summary>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} is end-of-life.</summary>
{% elsif event.type == "eoes" %}
    <title>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} end of extended support</title>
    <summary>{{ page.product_label | xml_escape }} {{ event.release_label | xml_escape }} extended support ended.</summary>
{% endif -%}
  </entry>
{% endfor -%}
</feed>


================================================
FILE: _layouts/product-list.html
================================================
---
layout: default
---
{%- if page.is_category %}
<h1>{{ page.title }}</h1>
{%- else %}
<h1>Products tagged with '{{ page.title }}'</h1>
{%- endif %}

{%- if page.is_category %}
<p>
{%- case page.id %}
{%- when 'app' %}This category lists desktop and mobile end-user applications.
{%- when 'database' %}This category lists database management systems, both relational and NoSQL.
{%- when 'device' %}This category lists devices, including hardware and IoT devices.
{%- when 'framework' %}This category lists software frameworks that provide a foundation for building applications.
{%- when 'lang' %}This category lists programming languages and their software development kits (SDKs).
{%- when 'os' %}This category lists operating systems, both desktop and server.
{%- when 'server-app' %}This category lists applications that are typically installed on a server.
{%- when 'service' %}This category lists managed service offerings (SaaS/PaaS...).
{%- when 'standard' %}This category lists widely used standards and protocols.
{%- endcase %}
</p>
{%- endif %}

{%- for product in page.products %}
<div class="product-list-item">
  <div class="d-flex flex-justify-between align-items-center">
    <div class="product-title">
      <h2>
        {%- include product-icon.html product=product size=30 %}
        <a href="{{ product.permalink }}">{{ product.title }}</a>
      </h2>

      <time datetime="{{ product.last_modified_at | date_to_xmlschema }}" class="fw-300">
        📅 Last updated on {{ product.last_modified_at | date_to_long_string }}
        {%- if product.auto and product.auto.methods %}
        <span title="Latest releases on this product are automatically updated.">🤖</span>
        {%- endif %}
      </time>
    </div>
    <span class="labels">
      {%- for tag in product.tags %}
        <a href="/tags/{{ tag }}"><span class="label">{{ tag }}</span></a>
      {%- endfor %}
    </span>
  </div>

  <div class="product-description">
    {{ product.content | extract_element:'blockquote' | first | extract_element:'p' }}
  </div>
</div>
{%- endfor %}


================================================
FILE: _layouts/product-tags.html
================================================
---
layout: default
---
<h1>{{ page.title }}</h1>

<ul class="tag-cloud" role="navigation" aria-label="Product tag cloud">
{% for tag_with_weight in page.tags %}
  {% assign tag = tag_with_weight | split:'|' | first %}
  {% assign weight = tag_with_weight | split:'|' | last %}
  <li><a href="/tags/{{ tag }}" data-weight="{{ weight }}">{{ tag }} ({{ weight }})</a></li>
{% endfor %}
</ul>


================================================
FILE: _layouts/product.html
================================================
---
layout: default
---

<div class="product-title">
  <div class="d-flex flex-justify-between align-items-center">
    <h1>{{ page.title }}</h1>
    <span class="labels">
      {%- for tag in page.tags %}
        <a href="/tags/{{ tag }}"><span class="label">{{ tag }}</span></a>
      {%- endfor %}
    </span>
  </div>

  <time datetime="{{ page.last_modified_at | date_to_xmlschema }}" class="fw-300">
    📅 Last updated on {{ page.last_modified_at | date_to_long_string }}
    {%- if page.auto and page.auto.methods %}
    <span title="Latest releases on this page are automatically updated.">🤖</span>
    {%- endif %}
  </time>
</div>

<div class="product-description">
  {% include product-icon.html product=page size=50 %}
  {{content | extract_element:'blockquote' | first | extract_element:'p' }}
</div>

{% if page.releaseImage %}
<img alt="Release Schedule Image Gantt Chart for {{page.title}}" src="{{page.releaseImage}}" />
{% endif %}

{%- capture now %}{{ "now" | date: "%s" | plus:0 }}{% endcapture %}
{%- assign customColumnsAfterRelease = page.customFields | where: 'display', 'after-release-column' %}
{%- assign customColumnsBeforeLatest = page.customFields | where: 'display', 'before-latest-column' %}
{%- assign customColumnsAfterLatest = page.customFields | where: 'display', 'after-latest-column' %}

<table class="lifecycle">
  <thead>
    <tr>
      <th>Release</th>{% assign colCount = 1 %}
      {% for column in customColumnsAfterRelease %}{% include custom-column-th.html column=column %}{% assign colCount = colCount | plus:1 %}{% endfor %}
      {% if page.releaseDateColumn %}<th>{{ page.releaseDateColumnLabel }}</th>{% assign colCount = colCount | plus:1 %}{% endif %}
      {% if page.discontinuedColumn %}<th>{{ page.discontinuedColumnLabel }}</th>{% assign colCount = colCount | plus:1 %}{% endif %}
      {% if page.eoasColumn %}<th>{{ page.eoasColumnLabel }}</th>{% assign colCount = colCount | plus:1 %}{% endif %}
      {% if page.eolColumn %}<th>{{ page.eolColumnLabel }}</th>{% assign colCount = colCount | plus:1 %}{% endif %}
      {% if page.eoesColumn %}<th>{{ page.eoesColumnLabel }}</th>{% assign colCount = colCount | plus:1 %}{% endif %}
      {% for column in customColumnsBeforeLatest %}{% include custom-column-th.html column=column %}{% assign colCount = colCount | plus:1 %}{% endfor %}
      {% if page.latestColumn %}<th>{{ page.latestColumnLabel }}</th>{% assign colCount = colCount | plus:1 %}{% endif %}
      {% for column in customColumnsAfterLatest %}{% include custom-column-th.html column=column %}{% assign colCount = colCount | plus:1 %}{% endfor %}
    </tr>
  </thead>

{% for r in page.releases %}
{%- assign releaseClasses = 'release' %}
{%- if r.can_be_hidden %}{% assign releaseClasses = 'release can-be-hidden' %}{% endif %}
  <tr class="{{ releaseClasses }}">
    {%- assign cycleColumnClass = '' %}
    {%- if r.is_eol %}{% assign cycleColumnClass = 'txt-linethrough' %}{% endif %}
    <td class="{{ cycleColumnClass }}">
      {% comment %}Only put a link in the version column if the release column is not shown{% endcomment %}
      {% if page.latestColumn == false and r.link  %}
        <a href="{{ r.link }}" title="Release Notes / Changelog for {{ r.label | strip_html }}">{{ r.label }}</a>
      {% else %}
        {{ r.label }}
      {% endif %}
    </td>

    {%- for column in customColumnsAfterRelease %}
    {% include custom-column-td.html release=r column=column cssClasses=cycleColumnClass %}
    {%- endfor %}

    {% if page.releaseDateColumn %}
    <td>{{ r.releaseDate | timeago }} <div>({{ r.releaseDate | date_to_string }})</div></td>
    {% endif %}

    {% if page.discontinuedColumn %}
    {%- assign colorClass = 'bg-green-000' %}
    {%- if r.is_almost_discontinued %}{% assign colorClass = 'bg-yellow-200' %}{% endif %}
    {%- if r.is_discontinued %}{% assign colorClass = 'bg-red-000' %}{% endif %}
    <td class="{{ colorClass }}">
    {% if r.discontinued_from %}
      {{ r.discontinued_from | timeago }} <div>({{ r.discontinued_from | date_to_string }})</div>
    {% else %}
      {% if r.is_discontinued %}Discontinued{% else %}In Production{% endif %}
    {% endif %}
    </td>
    {% endif %}

    {% if page.eoasColumn %}
    {%- assign colorClass = 'bg-green-000' %}
    {%- if r.is_almost_eoas %}{% assign colorClass = 'bg-yellow-200' %}{% endif %}
    {%- if r.is_eoas %}{% assign colorClass = 'bg-red-000' %}{% endif %}
    <td class="{{ colorClass }}">
    {% if r.eoas_from %}
      {% if r.is_eoas %}Ended{% else %}Ends{% endif %}
      {{ r.eoas_from | timeago }} <div>({{ r.eoas_from | date_to_string }})</div>
    {% else %}
      {% if r.is_eoas %}No{% else %}Yes{% endif %}
    {% endif %}
    </td>
    {% endif %}

    {% if page.eolColumn != false %}
    {%- assign colorClass = 'bg-green-000' %}
    {%- if r.is_almost_eol %}{% assign colorClass = 'bg-yellow-200' %}{% endif %}
    {%- if r.is_eol %}{% assign colorClass = 'bg-red-000' %}{% endif %}
    <td class="{{ colorClass }}">
      {% if r.eol_from %}
        {% if r.is_eol %}Ended{% else %}Ends{% endif %}
        {{ r.eol_from | timeago }} <div>({{ r.eol_from | date_to_string }})</div>
      {% else %}
        {% if r.is_eol %}No{% else %}Yes{% endif %}
      {% endif %}
    </td>
    {% endif %}

    {% if page.eoesColumn %}
    {%- assign colorClass = 'bg-green-000' %}
    {%- if r.is_almost_eoes != null and r.is_almost_eoes %}{% assign colorClass = 'bg-yellow-200' %}{% endif %}
    {%- if r.is_eoes != null and r.is_eoes %}{% assign colorClass = 'bg-red-000' %}{% endif %}
    {%- if r.eoes == null %}{% assign colorClass = 'bg-grey-lt-100' %}{% endif %}
    <td class="{{ colorClass }}">
      {% if r.eoes_from %}
        {% if r.is_eoes %}Ended{% else %}Ends{% endif %}
        {{ r.eoes_from | timeago }} <div>({{ r.eoes_from | date_to_string }})</div>
      {% else %}
        {% if r.is_eoes == null %}Unavailable{% else %}{% if r.is_eoes %}No{% else %}Yes{% endif %}{% endif %}
      {% endif %}
    </td>
    {% endif %}

    {%- for column in customColumnsBeforeLatest %}
    {% include custom-column-td.html release=r column=column %}
    {%- endfor %}

    {% if page.latestColumn != false %}
    {%- assign latestColumnClass = '' %}
    {%- if r.is_eol %}{% assign latestColumnClass = 'txt-linethrough' %}{% endif %}
    <td class="{{ latestColumnClass }}">
      {% if r.link %}
        <a href="{{ r.link }}" title="Release Notes / Changelog">{{ r.latest }}</a>
      {% else %}
        {{ r.latest }}
      {% endif %}
      {% if r.latestReleaseDate %}<div>({{ r.latestReleaseDate | date_to_string }})</div>{% endif %}
    </td>
    {% endif %}

    {%- for column in customColumnsAfterLatest %}
    {% include custom-column-td.html release=r column=column cssClasses=latestColumnClass %}
    {%- endfor %}
  </tr>
{% endfor %}
{% assign can_be_hidden_releases_count = page.releases | where: 'can_be_hidden', true | size %}
{% if can_be_hidden_releases_count > 0 %}
  <tr id="show-more-row" class="d-none">
    <td colspan="{{ colCount }}" class="text-center">
      <button id="show-hidden-releases-button" class="btn">
        Show more unmaintained releases
      </button>
    </td>
  </tr>
  <script type="text/javascript" src="assets/register-show-hidden-releases-handler.js" defer></script>
{% endif %}
</table>

<div class="policytext">
  {{ content | remove_first_element:'blockquote' }}
</div>

{% if page.releasePolicyLink %}
<p>More information is available on the <a href="{{page.releasePolicyLink}}">{{page.title}} website</a>.</p>
{% endif %}

{% if page.latestColumn %}
{% unless page.tags contains "discontinued" %}
<p>You should be running one of the supported release numbers listed above in the rightmost column.</p>
{% endunless %}
{% endif %}

{% if page.versionCommand %}
  <div id="version-command">
    <blockquote class="commandInfo">
      <p><pre>{{page.versionCommand}}</pre></p>
    </blockquote>
  </div>
{% endif %}

{% include identifiers.html %}

<hr>

<p>
  You can submit an improvement to this page
  <a href="{{page.permalink}}/_edit" title="Click the Pencil, the link takes you directly to the correct page">
    on GitHub&nbsp;<img class="emoji" title=":octocat:" alt=":octocat:" src="https://github.githubassets.com/images/icons/emoji/octocat.png" width="20" height="20">
  </a>.
  This page has also a corresponding <a title="Talk Page for {{page.title}}" href="https://github.com/endoflife-date/talk/wiki{{page.permalink}}">Talk Page 💬</a>.
</p>

<p>
  A JSON version of this page is available <a href="/api/v1/products{{page.permalink}}/">at /api/v1/products{{page.permalink}}/&nbsp;📡</a>.
  See <a href="/docs/api/v1/">the API Documentation&nbsp;📖</a> for more information.
  You can subscribe to the RSS feed <a href="{{page.permalink}}.atom" aria-label="Product events feed">at {{page.permalink}}.atom&nbsp;⚛️</a>
  or to the iCalendar feed <a href="webcal://{{site.url | split: '://' | last}}/calendar{{page.permalink}}.ics" aria-label="Product events calendar">at /calendar{{page.permalink}}.ics&nbsp;📅</a>.
</p>


================================================
FILE: _layouts/schema.html
================================================
{%- include variables.html -%}
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <script src="https://unpkg.com/@stoplight/elements/web-components.min.js"></script>
    <link rel="stylesheet" href="https://unpkg.com/@stoplight/elements/styles.min.css">
    <meta name="title" content="{{- title -}}">
    <meta name="description" content="{{- description -}}">
    <meta name="image" content="{{- image -}}">
    <meta name="theme-color" content="{{- site.theme_color -}}">
    <meta property="og:type" content="{{- site.og.type -}}">
    <meta property="og:site_name" content="{{- api.info.title -}}">
    <meta property="og:url" content="{{- url -}}">
    <meta property="og:title" content="{{- title -}}">
    <meta property="og:description" content="{{- description -}}">
    <meta property="og:image" content="{{- image -}}">
    <meta property="og:image:type" content="{{- site.og.image.type -}}">
    <meta property="og:image:width" content="{{- site.og.image.width -}}">
    <meta property="og:image:height" content="{{- site.og.image.height -}}">
    <meta property="og:image:alt" content="{{- api.info.title -}}">
    <meta property="twitter:card" content="{{- site.twitter.card -}}">
    <meta property="twitter:url" content="{{- url -}}">
    <meta property="twitter:title" content="{{- title -}}">
    <meta property="twitter:description" content="{{- description -}}">
    {% if site.twitter.site != '' %}<meta property="twitter:site" content="{{- site.twitter.site -}}">{% endif %}
    <meta property="twitter:image" content="{{- image -}}">
    <meta name="msapplication-config" content="{{- '/browserconfig.xml' | prepend: site.baseurl -}}">
    <meta name="msapplication-TileColor" content="{{- site.theme_color -}}">
    <title>{{- title -}}</title>
  </head>
  <body>
    <elements-api
    apiDescriptionUrl="/assets/openapi.yml"
    router="memory"
    ></elements-api>
  </body>
</html>


================================================
FILE: _layouts/swagger-ui.html
================================================
---
layout: null
---
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>{{ page.title }}</title>

  <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5/swagger-ui.css">
</head>

<body>
<div id="swagger-ui"></div>

<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@5/swagger-ui-standalone-preset.js"></script>
<script>
  window.onload = function () {
    const ui = SwaggerUIBundle({
      url: "{{ page.openapi_yml | absolute_url }}",
      dom_id: '#swagger-ui',
      deepLinking: true,
      presets: [SwaggerUIBundle.presets.apis, SwaggerUIStandalonePreset],
      plugins: [SwaggerUIBundle.plugins.DownloadUrl],
      layout: "BaseLayout"
    })
  }
</script>
</body>
</html>


================================================
FILE: _plugins/create-icalendar-files.rb
================================================
#!/usr/bin/env ruby

# This script creates an calendar/[product].ics file
# in each markdown source file, where [product] is the permalink value and

require 'fileutils'
require 'icalendar'
require 'yaml'

CALENDAR_DIR = 'calendar'.freeze

def load_yaml(file)
  if YAML.respond_to?(:unsafe_load)
    YAML.unsafe_load_file(file)
  else
    YAML.load_file(self[:encoded_value])
  end
end

class Product
  attr_reader :hash

  def initialize(markdown_file)
    @hash = load_yaml(markdown_file)
  end

  def permalink
    hash.fetch('permalink').sub('/', '')
  end

  def link
    "https://endoflife.date/#{permalink}"
  end

  def title
    hash.fetch('title')
  end

  def release_cycles
    hash.fetch('releases').map do |release|
      name = release.delete('releaseCycle')
      { 'name' => name, 'data' => release }
    end
  end
end

# return a icalendar output filename, including the directory name. Any / characters
# in the name are replaced with - to avoid file errors.
def icalendar_filename(output_dir, name)
  filename = name.to_s.tr('/', '-') + '.ics'
  File.join(output_dir, filename)
end

def notification_message(product, cycle, type)
  message = "#{product} #{cycle}"
  case type
  when 'eol' then
    message += ' will become End-of-life.'
  when 'eoas' then
    message += ' will end active development.'
  when 'releaseDate' then
    message += ' will be released.'
  when 'eoes' then
    message += ' will end extended support.'
  when 'discontinued' then
    message += ' will be discontinued.'
  end
end

def process_product(product)
  FileUtils.mkdir_p(CALENDAR_DIR)

  cal = Icalendar::Calendar.new
  product.release_cycles.each do |cycle|
    cycle.fetch('data').each do |key, item|
      next if !['releaseDate', 'eoas', 'eol', 'eoes', 'discontinued'].include?(key) || !item.instance_of?(Date)
      event = cal.event
      event.dtstart = Icalendar::Values::Date.new(item)
      event.dtend = Icalendar::Values::Date.new(item + 1)
      event.summary = "#{product.title} #{cycle.fetch('name')} #{key.upcase}"
      event.summary.ical_params = { 'altrep' => product.link }
      event.description = notification_message(product.title, cycle.fetch('name'), key)
      event.categories = [key]
      event.url = product.link
      next if key != 'eol'
      event.alarm do |a|
        a.action = 'DISPLAY'
        a.trigger = Icalendar::Values::DateTime.new((item << 12).to_datetime + Rational(9, 24))
      end
      event.alarm do |a|
        a.action = 'DISPLAY'
        a.trigger = Icalendar::Values::DateTime.new((item << 6).to_datetime + Rational(9, 24))
      end
      event.alarm do |a|
        a.action = 'DISPLAY'
        a.trigger = Icalendar::Values::DateTime.new((item << 3).to_datetime + Rational(9, 24))
      end
      event.alarm do |a|
        a.action = 'DISPLAY'
        a.trigger = Icalendar::Values::DateTime.new((item << 1).to_datetime + Rational(9, 24))
      end
      event.alarm do |a|
        a.action = 'DISPLAY'
        a.trigger = '-P6DT9H'
      end
      event.alarm do |a|
        a.action = 'DISPLAY'
        a.trigger = 'PT9H'
      end
    end
  end
  output_file = icalendar_filename(CALENDAR_DIR, product.permalink)
  File.open(output_file, 'w') { |f| f.puts cal.to_ical }
end

# each file is something like 'products/foo.md'
def process_all_files()
  Dir['products/*.md'].each do |file|
    product = Product.new(file)
    process_product(product)
  end
end

############################################################

process_all_files()


================================================
FILE: _plugins/end-of-life-filters.rb
================================================
require 'nokogiri'

# Various custom filters used by endoflife.date.
#
# All the filters has been gathered in the same module to avoid module name clashing
# (see https://github.com/endoflife-date/endoflife.date/issues/2074).
module EndOfLifeFilter

  # Enables Liquid templating in front-matter.
  # See https://fettblog.eu/snippets/jekyll/liquid-in-frontmatter/.
  def liquify(input)
    Liquid::Template.parse(input).render(@context)
  end

  # Parse a URI and return a relevant part
  #
  # Usage:
  # {{ page.url | parse_uri:'host' }}
  # {{ page.url | parse_uri:'scheme' }}
  # {{ page.url | parse_uri:'userinfo' }}
  # {{ page.url | parse_uri:'port' }}
  # {{ page.url | parse_uri:'registry' }}
  # {{ page.url | parse_uri:'path' }}
  # {{ page.url | parse_uri:'opaque' }}
  # {{ page.url | parse_uri:'query' }}
  # {{ page.url | parse_uri:'fragment' }}
  def parse_uri(uri_str, part='host')
    URI::parse(uri_str).send(part.to_s)
  end

  # Extract the elements of the given kind from the HTML.
  def extract_element(html, element)
    entries = []

    @doc = Nokogiri::HTML::DocumentFragment.parse(html)
    @doc.css(element).each do |node|
      entries << node.to_html
    end

    entries
  end

  # Removes the first element of the given kind from the HTML.
  def remove_first_element(html, element)
    doc = Nokogiri::HTML::DocumentFragment.parse(html)
    e = doc.css(element)
    e.first.remove if e&.first
    doc.to_html
  end

  # Remove the '.0' if the input ends with '.0', else do nothing.
  #
  # Usage:
  # {{ '2.1.0' | drop_zero_patch }} => '2.1'
  # {{ '2.1.1' | drop_zero_patch }} => '2.1.1'
  def drop_zero_patch(input)
    input.delete_suffix(".0")
  end

  # Collapse the given cycles according to the given field.
  #
  # Cycle fields are transformed to a cycle range using the given range_separator. For example if
  # cycles are [1, 2, 3] and the separator is " -> ", the cycle range will be "1 -> 3".
  #
  # Usage:
  # cycles = [
  #   {releaseCycle:'1', java:'8', other:'a'},
  #   {releaseCycle:'2', java:'8', other:'b'},
  #   {releaseCycle:'3', java:'11', other:'c'},
  #   {releaseCycle:'4', java:'11', other:'d'},
  #   {releaseCycle:'5', java:'11', other:'d'},
  #   {releaseCycle:'6', java:'17', other:'e'}
  # ]
  #
  # {{ cycles | collapse:'java',' -> ' }}
  # => [{releaseCycle:'1 -> 2', java:'8'}, {releaseCycle:'3 -> 5', java:'11'}, {releaseCycle:'6', java:'17'}]
  def collapse_cycles(cycles, field, range_separator)
    cycles
      .to_h { |e| [e['releaseCycle'], e[field]] }
      .group_by { |releaseCycle, value| value } # see https://stackoverflow.com/a/18841831/374236
      .map { |value, entries|
        cycles = entries.map { |e| e[0] }.sort_by { |cycle| Gem::Version.new(cycle) }
        cycles.length == 1 ? [cycles.first.to_s, value] : [cycles.first.to_s + range_separator + cycles.last.to_s, value]
      }
      .map { |cycleRange, value| Hash['releaseCycle', cycleRange, field, value] }
  end

  # Compute the number of days from now to the given date.
  #
  # Usage (assuming now is '2023-01-01'):
  # {{ '2023-01-10' | days_from_now }} => 9
  # {{ '2023-01-01' | days_from_now }} => 0
  # {{ '2022-12-31' | days_from_now }} => -1
  def days_from_now(from)
    from_timestamp = Date.parse(from.to_s).to_time.to_i
    to_timestamp = Date.today.to_time.to_i
    return (from_timestamp - to_timestamp) / (60 * 60 * 24)
  end

  # Compute the color according to the given number of days until the end.
  #
  # Usage:
  # {{ true | end_color }} => bg-green-000
  # {{ false | end_color }} => bg-red-000
  # {{ -1 | end_color }} => bg-green-000
  # {{ 1 | end_color }} => bg-yellow-200
  # {{ 365 | end_color }} => bg-red-000
  # {{ '2025-01-01' | days_from_now | end_color }} => bg-green-000
  # {{ '2023-01-02' | days_from_now | end_color }} => bg-yellow-200
  # {{ '2021-01-01' | days_from_now | end_color }} => bg-red-000
  # {{ '2025-01-01' | end_color }} => bg-green-000
  def end_color(input)
    if input == true
      return 'bg-green-000'
    elsif input == false
      return 'bg-red-000'
    elsif input.is_a? Integer
      if input < 0
        return 'bg-red-000'
      elsif input < 120
        return 'bg-yellow-200'
      else
        return 'bg-green-000'
      end
    else
      # Assuming it's a date
      return end_color(days_from_now(input))
    end
  end
end

Liquid::Template.register_filter(EndOfLifeFilter)


================================================
FILE: _plugins/end-of-life.rb
================================================
# All categories on endoflife.date.
# This also defines the order in which they appear in the navigation, so keep ordered alphabetically.
CATEGORIES = %w[app database device framework lang os server-app service standard]

def is_category?(name)
  CATEGORIES.include?(name)
end

# Transform a tag name to a title.
# By default the name is used as the title and this is overridden for tags that are categories so that
# so that the navigation is more user-friendly.
def tag_title(tag_name)
  case tag_name
  when 'app' then 'Applications'
  when 'database' then 'Databases'
  when 'device' then 'Devices'
  when 'framework' then 'Frameworks'
  when 'lang' then 'Languages'
  when 'os' then 'Operating Systems'
  when 'server-app' then 'Server Applications'
  when 'service' then 'Services'
  when 'standard' then 'Standards'
  else
    tag_name
  end
end

def category_index(category_name)
  CATEGORIES.index(category_name)
end


================================================
FILE: _plugins/generate-api-v0.rb
================================================
#!/usr/bin/env ruby

# This script creates an api/[product]/[version].json file for each releaseCycle
# in each markdown source file, where [product] is the permalink value and
# [version] is the releaseCycle value.
#
# The contents of the JSON files is the data in the releases, minus the
# releaseCycle.

require 'fileutils'
require 'json'
require 'yaml'
require 'date'

API_DIR = 'api'.freeze

def load_yaml(file)
  if YAML.respond_to?(:unsafe_load)
    YAML.unsafe_load_file(file)
  else
    YAML.load_file(self[:encoded_value])
  end
end

class Product
  attr_reader :hash

  def initialize(markdown_file)
    @hash = load_yaml(markdown_file)
  end

  def permalink
    hash.fetch('permalink').sub('/', '')
  end

  def release_cycles
    hash.fetch('releases').map do |release|
      name = release.delete('releaseCycle')
      release['lts'] = release['lts'] || false

      # To keep backward compatibility following the renaming of support and extendedSupport fields.
      # See https://github.com/endoflife-date/endoflife.date/issues/4923.
      if release.has_key?('eoas')
        eoas = release.delete('eoas')
        release['support'] = eoas.respond_to?(:strftime) ? eoas : !eoas
      end
      if hash.has_key?('eoesColumn')
        if release.has_key?('eoes')
          eoes = release.delete('eoes')
          release['extendedSupport'] = eoes.respond_to?(:strftime) ? eoes : !eoes
        else
          release['extendedSupport'] = false
        end
      end

      { 'name' => name, 'data' => release }
    end
  end
end

# return a json output filename, including the directory name. Any / characters
# in the name are replaced with - to avoid file errors.
def json_filename(output_dir, name)
  filename = name.to_s.tr('/', '-') + '.json'
  File.join(output_dir, filename)
end

def process_product(product)
  output_dir = File.join(API_DIR, product.permalink)
  FileUtils.mkdir_p(output_dir) unless FileTest.directory?(output_dir)

  all_cycles = []
  product.release_cycles.each do |cycle|
    output_file = json_filename(output_dir, cycle.fetch('name'))
    File.open(output_file, 'w') { |f| f.puts cycle.fetch('data').to_json }
    all_cycles.append({'cycle' => cycle.fetch('name')}.merge(cycle.fetch('data')))
  end
  output_file = json_filename(API_DIR, product.permalink)
  File.open(output_file, 'w') { |f| f.puts all_cycles.to_json }
end

# each file is something like 'products/foo.md'
def process_all_files()
  all_products = []
  Dir['products/*.md'].each do |file|
    product = Product.new(file)
    product_cycles = process_product(product)
    all_products.append(product.permalink)
  end
  output_file = json_filename(API_DIR, 'all')
  File.open(output_file, 'w') { |f| f.puts all_products.sort.to_json }
end

############################################################

process_all_files()


================================================
FILE: _plugins/generate-api-v1.rb
================================================
# This script creates API files for version 1 of the endoflife.date API.
#
# There are multiples endpoints :
#
# - /api/v1 - list all major endpoints (those not requiring a parameter)
# - /api/v1/products - list all products (summary)
# - /api/v1/products/full - list all products (full information)
# - /api/v1/products/<product> - get a single product details
# - /api/v1/products/<product>/latest - get details on the latest release cycle for the given product
# - /api/v1/products/<product>/<release> - get details on the given release cycle for the given product
# - /api/v1/categories - list categories used on endoflife.date
# - /api/v1/categories/<category> - list products having the given category
# - /api/v1/tags - list tags used on endoflife.date
# - /api/v1/tags/<tag> - list products having the given tag
# - /api/v1/identifiers - list all identifiers
# - /api/v1/identifiers/<identifier> - retrieve all Products that are identified by the given Identifier.


require 'jekyll'

module ApiV1

  # This version must be kept in sync with the version in api_v1/openapi.yml.
  VERSION = '1.2.0'
  MAJOR_VERSION = VERSION.split('.')[0]

  STRIP_HTML_BLOCKS = Regexp.union(
    /<script.*?<\/script>/m,
    /<!--.*?-->/m,
    /<style.*?<\/style>/m
  )
  STRIP_HTML_TAGS = /<.*?>/m

  # Remove HTML from a string (such as an LTS label).
  # This is the equivalent of Liquid::StandardFilters.strip_html, which cannot be used
  # unfortunately.
  def self.strip_html(input)
    empty = ''.freeze
    result = input.to_s.gsub(STRIP_HTML_BLOCKS, empty)
    result.gsub!(STRIP_HTML_TAGS, empty)
    result
  end

  def self.site_url(site, path)
    "#{site.config['url']}#{path}"
  end

  def self.api_url(site, path)
    site_url(site, "/api/v#{ApiV1::MAJOR_VERSION}#{path}")
  end

  class ApiGenerator < Jekyll::Generator
    safe true
    priority :lowest

    TOPIC = "API " + ApiV1::VERSION + ":"

    def generate(site)
      @site = site
      start = Time.now
      Jekyll.logger.info TOPIC, "Generating..."

      product_pages = site.pages.select { |page| page.data['layout'] == 'product' }
      add_index_page(site)
      add_products_related_pages(site, product_pages)
      add_categories_related_pages(site, product_pages)
      add_tags_related_pages(site, product_pages)
      add_identifiers_related_pages(site, product_pages)

      Jekyll.logger.info TOPIC, "Done in #{(Time.now - start).round(3)} seconds."
    end

    private

    def add_index_page(site)
      site.pages << JsonPage.of_raw_data(site, '/', [
        { name: "products", uri: "#{ApiV1.api_url(site, '/products')}" },
        { name: "categories", uri: "#{ApiV1.api_url(site, '/categories')}" },
        { name: "tags", uri: "#{ApiV1.api_url(site, '/tags')}" },
      ], { total: 3 })
    end

    def add_products_related_pages(site, products)
      add_all_products_page(site, products)
      add_all_products_and_releases_page(site, products)

      products.each do |page|
        add_product_page(site, page)
        add_latest_release_page(site, page)
        page.data['releases'].each { |release| add_release_page(site, page, release) }
      end
    end

    def add_all_products_page(site, products)
      site.pages << JsonPage.of_products_summary(site, '/products/', products)
    end

    def add_all_products_and_releases_page(site, products)
      site.pages << JsonPage.of_products_details(site, '/products/full/', products)
    end

    def add_product_page(site, product)
      site.pages << JsonPage.of_product(site, product)
    end

    def add_latest_release_page(site, page)
      latest = page.data['releases'][0]
      site.pages << JsonPage.of_release(site, page, latest, 'latest')
    end

    def add_release_page(site, page, release)
      site.pages << JsonPage.of_release(site, page, release)
    end

    def add_categories_related_pages(site, products)
      products_by_category = products_by_category(products)

      add_all_categories_page(site, products_by_category.keys)
      products_by_category.each do |category, products|
        add_category_page(site, category, products)
      end
    end

    def products_by_category(products)
      products_by_category = {}
      products.each { |product| add_to_map(products_by_category, product.data['category'], product) }
      products_by_category
    end

    def add_category_page(site, category, products)
      site.pages << JsonPage.of_products_summary(site, "/categories/#{category}", products)
    end

    def add_all_categories_page(site, categories)
      data = categories.map { |category| { name: category, uri: "#{ApiV1.api_url(site, "/categories/#{category}")}" }}
      meta = { total: categories.size() }
      site.pages << JsonPage.of_raw_data(site, '/categories/', data, meta)
    end

    def add_tags_related_pages(site, products)
      products_by_tag = products_by_tag(products)

      add_all_tags_page(site, products_by_tag.keys)
      products_by_tag.each do |tag, products|
        add_tag_page(site, tag, products)
      end
    end

    def products_by_tag(products)
      products_by_tag = {}
      products.each do |product|
        product.data['tags'].each { |tag| add_to_map(products_by_tag, tag, product) }
      end
      products_by_tag
    end

    def add_tag_page(site, tag, products)
      site.pages << JsonPage.of_products_summary(site, "/tags/#{tag}", products)
    end

    def add_all_tags_page(site, tags)
      data = tags.map { |tag| { name: tag, uri: "#{ApiV1.api_url(site, "/tags/#{tag}")}" }}
      meta = { total: tags.size() }
      site.pages << JsonPage.of_raw_data(site, '/tags/', data, meta)
    end

    def add_identifiers_related_pages(site, products)
      identifiers_by_type = identifiers_by_type(site, products)

      add_all_identifier_types_page(site, identifiers_by_type.keys)
      identifiers_by_type.each do |identifier_kind, identifiers|
        add_identifiers_for_type_page(site, identifier_kind, identifiers)
      end
    end

    def identifiers_by_type(site, products)
      identifiers_by_type = {}
      products.each do |product|
        product.data['identifiers'].each do |identifier|
          add_to_map(identifiers_by_type, identifier.keys.first, {
            identifier: identifier.values.first,
            product: {
              name: product.data['id'],
              uri: ApiV1.api_url(site, "/products/#{product.data['id']}")
            }
          })
        end
      end
      identifiers_by_type
    end

    def add_all_identifier_types_page(site, types)
      data = types.map { |type| { name: type, uri: "#{ApiV1.api_url(site, "/identifiers/#{type}/")}" }}
      meta = { total: types.size() }
      site.pages << JsonPage.of_raw_data(site, '/identifiers/', data, meta)
    end

    def add_identifiers_for_type_page(site, type, identifiers)
      meta = { total: identifiers.length }
      site.pages << JsonPage.of_raw_data(site, "/identifiers/#{type}", identifiers, meta)
    end

    def add_to_map(map, key, page)
      if map.has_key? key
        map[key] << page
      else
        map[key] = [page]
      end
    end
  end

  class JsonPage < Jekyll::Page
    class << self
      private :new

      def of_raw_data(site, path, data, metadata = {})
        new(site, path, data, metadata)
      end

      def of_products_summary(site, path, products)
        data = products.map { |product| product_summary_to_json(site, product) }
        meta = { total: products.size() }
        new(site, path, data, meta)
      end

      def of_products_details(site, path, products)
        data = products.map { |product| product_to_json(site, product) }
        meta = { total: products.size() }
        new(site, path, data, meta)
      end

      def of_product(site, product)
        path = "/products/#{product.data['id']}"
        data = product_to_json(site, product)
        meta = {
          # https://github.com/gjtorikian/jekyll-last-modified-at/blob/master/lib/jekyll-last-modified-at/determinator.rb
          last_modified: product.data['last_modified_at'].last_modified_at_time.iso8601,
        }
        new(site, path, data, meta)
      end

      def of_release(site, product, release, identifier = nil)
        name = identifier ? identifier : release['id']
        path = "/products/#{product.data['id']}/releases/#{name}"
        data = release_to_json(product, release)
        new(site, path, data, {})
      end

      def product_to_json(site, product)
        additional_details = {
          versionCommand: product.data['versionCommand'],
          identifiers: product.data['identifiers'].map { |identifier| {
            type: identifier.keys.first,
            id: identifier.values.first
          } },
          labels: {
            "eoas": product.data['eoasColumn'] ? ApiV1.strip_html(product.data['eoasColumnLabel']) : nil,
            "discontinued": product.data['discontinuedColumn'] ? ApiV1.strip_html(product.data['discontinuedColumnLabel']) : nil,
            "eol": product.data['eolColumn'] ? ApiV1.strip_html(product.data['eolColumnLabel']) : nil,
            "eoes": product.data['eoesColumn'] ? ApiV1.strip_html(product.data['eoesColumnLabel']) : nil,
          },
          links: {
            icon: product.data['iconUrl'],
            html: ApiV1.site_url(site, "/#{product.data['id']}"),
            releasePolicy: product.data['releasePolicyLink'],
          },
          releases: product.data['releases'].map { |release| release_to_json(product, release) }
        }

        product_summary_to_json(site, product).except(:uri).merge(additional_details)
      end

      def product_summary_to_json(site, product)
        {
          name: product.data['id'],
          aliases: product.data['aliases'],
          label: product.data['title'],
          category: product.data['category'],
          tags: product.data['tags'],
          uri: ApiV1.api_url(site, "/products/#{product.data['id']}")
        }
      end

      def release_to_json(product, release)
        json = {
          name: release['releaseCycle'],
          codename: release['codename'],
          label: ApiV1.strip_html(release['label']),
          releaseDate: release['releaseDate'],
          isLts: release['is_lts'],
          ltsFrom: release['lts_from'],
          isEoas: release['is_eoas'],
          eoasFrom: release['eoas_from'],
          isEol: release['is_eol'],
          eolFrom: release['eol_from'],
          isDiscontinued: release['is_discontinued'],
          discontinuedFrom: release['discontinued_from'],
          isEoes: release['is_eoes'],
          eoesFrom: release['eoes_from'],
          isMaintained: release['is_maintained'],
          latest: {
            name: release['latest'],
            date: release['latestReleaseDate'],
            link: release['link'],
          },
          custom: custom_fields(product, release)
        }

        if !product.data['eoasColumn']
          json.delete(:isEoas)
          json.delete(:eoasFrom)
        end

        if !product.data['discontinuedColumn']
          json.delete(:isDiscontinued)
          json.delete(:discontinuedFrom)
        end

        if !product.data['eoesColumn']
          json.delete(:isEoes)
          json.delete(:eoesFrom)
        end

        if !product.data['latestColumn']
          json[:latest] = nil
        end

        if product.data['customFields'].empty?
          json[:custom] = nil
        end

        json
      end

      def custom_fields(product, release)
        json = {}
        product.data['customFields'].map { |column| column['name'] }.map { |name| json[name] = release[name] }
        json
      end
    end

    def initialize(site, path, data, metadata)
      @site = site
      @base = site.source
      @dir = "api/v#{ApiV1::MAJOR_VERSION}#{path}"
      @name = "index.json"
      @data = {}
      @data['layout'] = 'json'

      @data['data'] = {}
      @data['data']['schema_version'] = ApiV1::VERSION
      @data['data']['generated_at'] = site.time.iso8601
      @data['data'].merge!(metadata)
      @data['data']['result'] = data

      self.process(@name)
    end
  end
end


================================================
FILE: _plugins/generate-product-feeds.rb
================================================
# This script creates product pages for the website.

require 'jekyll'

module EndOfLife

  def self.site_url(site, path)
    "#{site.config['url']}#{path}"
  end

  class ProductFeedsGenerator < Jekyll::Generator
    safe true
    priority :lowest

    TOPIC = "Product feeds:"

    def generate(site)
      @site = site
      start = Time.now
      Jekyll.logger.info TOPIC, "Generating..."

      site.pages.select { |page| page.data['layout'] == 'product' }.each do |product|
        site.pages << ProductFeed.new(site, product)
      end

      site.pages << NewProductsFeed.new(site)

      Jekyll.logger.info TOPIC, "Done in #{(Time.now - start).round(3)} seconds."
    end
  end

  class ProductFeed < Jekyll::Page
    def initialize(site, product)
      @site = site
      @base = site.source
      @dir = ""
      @name = "#{product.data['id']}.atom"

      events = []
      product.data['releases'].each do |release|
        release_name = release['releaseCycle']
        release_label = release['label']

        release_date = release['releaseDate']
        events << {
          "type" => "release",
          "release_name" => release_name,
          "release_label" => release_label,
          "occurred_at" => release_date&.to_datetime&.beginning_of_day,
        }

        eoas_date = release['eoas']
        if eoas_date && eoas_date.is_a?(Date) then
          events << {
            "type" => "eoas",
            "release_name" => release_name,
            "release_label" => release_label,
            "occurred_at" => eoas_date&.to_datetime&.end_of_day,
          }
        end

        eol_date = release['eol']
        if eol_date && eol_date.is_a?(Date) then
          events << {
            "type" => "eol",
            "release_name" => release_name,
            "release_label" => release_label,
            "occurred_at" => eol_date&.to_datetime&.end_of_day,
          }

          eol_date_7d = release['eol'] - 7
          events << {
            "type" => "eol-7d",
            "release_name" => release_name,
            "release_label" => release_label,
            "occurred_at" => eol_date_7d&.to_datetime&.end_of_day,
          }
        end

        eoes_date = release['eoes']
        if eoes_date && eoes_date.is_a?(Date) then
          events << {
            "type" => "eoes",
            "release_name" => release_name,
            "release_label" => release_label,
            "occurred_at" => eoes_date&.to_datetime&.end_of_day,
          }
        end
      end

      @data = {
        "layout" => "product-feed",
        "product_id" => product.data['id'],
        "product_label" => product.data['title'],
        "product_link" => EndOfLife.site_url(site, product.data['permalink']),
        "last_updated" => product.data['last_modified_at'],
        "events" => events.select { |event| event["occurred_at"] <= Time.now },
        "nav_exclude" => true
      }

      self.process(@name)
    end
  end

  class NewProductsFeed < Jekyll::Page
    def initialize(site)
      @site = site
      @base = site.source
      @dir = ""
      @name = "new-products.atom"

      products = site.pages
        .select { |p| p.data['layout'] == 'product' && p.data['addedAt'] }
        .map { |p|
          {
            "title"    => p.data['title'],
            "link"     => EndOfLife.site_url(site, p.data['permalink']),
            "added_at" => p.data['addedAt'].to_datetime.beginning_of_day,
          }
        }
        .sort_by { |p| p["added_at"] }
        .reverse

      @data = {
        "layout"      => "new-products-feed",
        "products"    => products,
        "nav_exclude" => true,
      }

      self.process(@name)
    end
  end
end


================================================
FILE: _plugins/generate-tag-pages.rb
================================================
# This script create the tag (and categories, because they are also tags) pages for the website.

require 'jekyll'
require_relative 'end-of-life'

module EndOfLife

  class ProductPagesGenerator < Jekyll::Generator
    safe true
    priority :lowest

    TOPIC = "Tag pages:"

    def generate(site)
      @site = site
      start = Time.now
      Jekyll.logger.info TOPIC, "Generating..."

      products = site.pages.select { |page| page.data['layout'] == 'product' }

      products_by_tag = products_by_tag(products)
      site.pages << TagsPage.new(site, products_by_tag)
      products_by_tag.each do |tag, products_for_tag|
        site.pages << TagPage.new(site, tag, products_for_tag)
      end

      Jekyll.logger.info TOPIC, "Done in #{(Time.now - start).round(3)} seconds."
    end

    def products_by_tag(products)
      products_by_tag = {}
      products.each do |product|
        product.data['tags'].each { |tag| add_to_map(products_by_tag, tag, product) }
      end
      products_by_tag
    end

    def add_to_map(map, key, page)
      if map.has_key? key
        map[key] << page
      else
        map[key] = [page]
      end
    end
  end

  class TagsPage < Jekyll::Page
    def initialize(site, products_by_tag)
      @site = site
      @base = site.source
      @dir = "tags"
      @name = "index.html"

      tags = products_by_tag.map { |tag, value| "#{tag}|#{value.size()}" }.sort
      @data = {
        "title" => "All tags",
        "layout" => "product-tags",
        "permalink" => "/tags/",
        "has_toc" => false,
        "nav_order"=> 9999, # Ensure this page appears last in the navigation
        "tags" => tags
      }

      self.process(@name)
    end
  end

  class TagPage < Jekyll::Page
      def initialize(site, tag, products)
        @site = site
        @base = site.source
        @dir = "tags"
        @name = "#{tag}.html"

        is_category = is_category?(tag)
        @data = {
          "id" => tag,
          "title" => tag_title(tag),
          "layout" => "product-list",
          "permalink" => "/tags/#{tag}",
          "has_toc" => false,
          "parent" => is_category ? nil: "All tags",
          "nav_order"=> is_category ? category_index(tag) : nil, # Ensure category pages appears first in the navigation, order by their name
          "is_category" => is_category,
          "products" => products.sort_by { |product| product.data['title'] }
        }

        self.process(@name)
      end
    end
end


================================================
FILE: _plugins/identifier-to-url.rb
================================================
require 'package_url'
require 'pp'
require 'jekyll'

# Generate URLs for different package type, raising an error if the type is unknown or the identifier invalid.
class IdentifierToUrl

  def render(identifier_hash)
    if identifier_hash.size != 1 or not identifier_hash.values[0].kind_of?(String)
      raise "expecting an identifier hash with a single string value, got #{identifier_hash}"
    end

    type = identifier_hash.keys[0]
    identifier = identifier_hash.values[0]
    if ['cpe'].include?(type)
      # Regex found on https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd.
      # Regex for 2.3 has been simplified as I could not make it work with Ruby.
      cpe2_2_regex = /^[c][pP][eE]:\/[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6}$/
      if identifier.match(cpe2_2_regex)
        # No known way to generate URLs for CPEs
        return nil
      end

      cpe2_3_regex = /^[c][pP][eE]:2\.3:[AHOaho]?(:[A-Za-z0-9\._\-~%]*){0,6}$/
      if identifier.match(cpe2_3_regex)
        return "https://services.nvd.nist.gov/rest/json/cpes/2.0?cpeMatchString=#{identifier}"
      end

      raise "Invalid CPE: should match either #{cpe2_2_regex} for CPE 2.2 or #{cpe2_3_regex} for CPE 2.3"

    elsif type == 'repology'
      return _build_repology_url(identifier)

    elsif type == 'purl'
      begin
        purl = PackageURL.parse(identifier)
        raise "Cannot handle PURL with no name or type: #{identifier}" unless purl.type and purl.name # should be impossible

        method_name = "_build_#{purl.type}_url"
        raise "Missing method handler #{method_name} for PURL type #{purl.type}" unless respond_to?(method_name)
        return send(method_name, purl)
      rescue => e
        raise "Invalid PURL identifier: #{identifier} : #{e}"
      end

    else
      raise "Unsupported identifier type: #{type}"
    end
  end

  def _build_repology_url(identifier)
    return "https://repology.org/project/#{identifier}"
  end

  def _build_cargo_url(purl)
    return "https://crates.io/crates/#{purl.name}"
  end

  def _build_docker_url(purl)
    raise "Unsupported docker PURL #{purl}: no namespace specified" unless purl.namespace
    name = purl.namespace == 'library' ? "_/#{purl.name}" : "r/#{purl.namespace}/#{purl.name}" # avoid redirects
    return "https://hub.docker.com/#{name}"
  end

  def _build_github_url(purl)
    raise "Unsupported github PURL #{purl}: no namespace specified" unless purl.namespace
    return "https://github.com/#{purl.namespace}/#{purl.name}"
  end

  def _build_bitbucket_url(purl)
    raise "Unsupported bitbucket PURL #{purl}: no namespace specified" unless purl.namespace
    return "https://bitbucket.org/#{purl.namespace}/#{purl.name}"
  end

  def _build_gitlab_url(purl)
    raise "Unsupported gitlab PURL #{purl}: no namespace specified" unless purl.namespace
    return "https://gitlab.com/#{purl.namespace}/#{purl.name}"
  end

  def _build_gem_url(purl)
    return "https://rubygems.org/gems/#{purl.name}"
  end

  def _build_cran_url(purl)
    return "https://cran.r-project.org/web/packages/#{purl.name}/index.html"
  end

  def _build_npm_url(purl)
    name = purl.namespace ? "#{purl.namespace}/#{purl.name}" : purl.name
    return "https://www.npmjs.com/package/#{name}"
  end

  def _build_pypi_url(purl)
    return "https://pypi.org/project/#{purl.name}"
  end

  def _build_composer_url(purl)
    raise "Unsupported composer PURL #{purl}: no namespace specified" unless purl.namespace
    return "https://packagist.org/packages/#{purl.namespace}/#{purl.name}"
  end

  def _build_nuget_url(purl)
    name = purl.namespace ? "#{purl.namespace}.#{purl.name}" : purl.name
    return "https://www.nuget.org/packages/#{name}"
  end

  def _build_hackage_url(purl)
    return "https://hackage.haskell.org/package/#{purl.name}"
  end

  def _build_hex_url(purl)
    return "https://hex.pm/packages/#{purl.name}"
  end

  def _build_golang_url(purl)
    raise "Unsupported golang PURL #{purl}: no namespace specified" unless purl.namespace
    return "https://pkg.go.dev/#{purl.namespace}/#{purl.name}"
  end

  def _build_scoop_url(purl)
    return "https://scoop.sh/#/apps?q=#{purl.name}"
  end

  def _build_oci_url(purl)
    raise "Unsupported oci PURL #{purl}: no repository_url qualifier specified" unless purl.qualifiers and purl.qualifiers.key?('repository_url')
    repository_url = purl.qualifiers['repository_url'].gsub(/https?:\/\//, '') # ensure there is no http:// or https:// in repository_url
    return "https://#{repository_url}/#{purl.name}"
  end

  def _build_chocolatey_url(purl)
    return "https://chocolatey.org/packages/#{purl.name}"
  end

  def _build_brew_url(purl)
    return "https://formulae.brew.sh/formula/#{purl.name}"
  end

  def _build_winget_url(purl)
    return "https://winget.run/pkg/#{purl.name}"
  end

  def _build_maven_url(purl)
    raise "Unsupported maven PURL #{purl}: no namespace specified" unless purl.namespace
    return "https://search.maven.org/artifact/#{purl.namespace}/#{purl.name}"
  end

  def _build_apk_url(purl)
    if purl.qualifiers and purl.qualifiers.key?('repository_url')
      return nil # allowed but don't know how to generate the correct URL
    end

    if purl.namespace == 'alpine'
      return "https://pkgs.alpinelinux.org/packages?name=#{purl.name}"
    end

    if ['openwrt', 'wolfi'].include?(purl.namespace)
      return nil # allowed but no known URL
    end

    raise "Unsupported apk PURL #{purl}: unknown namespace #{purl.namespace}"
  end

  def _build_deb_url(purl)
    if purl.qualifiers and purl.qualifiers.key?('repository_url')
      return nil # allowed but don't know how to generate the correct URL
    end

    if purl.qualifiers and purl.qualifiers.key?('distro')
      distro = purl.qualifiers['distro']
      if ["bookworm", "bullseye", "buster", "trixie", "sid"].include?(distro)
        return "https://packages.debian.org/#{distro}/source/#{purl.name}"
      elsif ["focal", "jammy", "mantic", "noble"].include?(distro)
        return "https://packages.ubuntu.com/#{distro}/#{purl.name}"
      else
        raise "Unsupported deb PURL #{purl}: distro #{distro} not listed on the packages website anymore"
      end
    end

    # Probably an official package in an old ubuntu/debian distro
    if purl.namespace == 'ubuntu'
      return "https://launchpad.net/ubuntu/+source/#{purl.name}"
    elsif purl.namespace == 'debian'
      return "https://sources.debian.org/src/#{purl.name}/"
    end
  end

  def _build_rpm_url(purl)
    if purl.qualifiers and purl.qualifiers.key?('repository_url')
      return nil # allowed but don't know how to generate the correct URL
    end

    if purl.namespace == 'fedora'
      return "https://packages.fedoraproject.org/pkgs/#{purl.name}/"
    end

    if ['amzn', 'centos', 'opensuse', 'redhat'].include?(purl.namespace)
      return nil # allowed but no known URL
    end

    raise "Unsupported rpm PURL #{purl}: unknown namespace #{purl.namespace}"
  end

  def _build_swid_url(purl)
    return nil # valid, but don't know how to generate this kind of URL
  end

  def _build_generic_url(purl)
    return nil # valid, but don't know how to generate this kind of URL
  end

  def _build_alpm_url(purl)
    return "https://archlinux.org/packages/?q=#{purl.name}"
  end
end


================================================
FILE: _plugins/product-data-enricher.rb
================================================
# This plugin enriches the product pages by setting or precomputing its metadata, so that it can be
# easily consumed in layouts or plugins (such as the API v1 plugin).
#
# Naming conventions:
# - Raw fields, declared in product's markdown front matter or derived from a template (such as the
#   changelogTemplate), use the camel case notation (example: endOfLife).
# - Computed fields, injected by ProductDataEnricher, use the snake case notation (example: end_of_life).
#
# Here is a list of computed fields :
# - is_maintained (in cycles) : whether the release cycle is still supported (mandatory)
# - can_be_hidden (in cycles) : whether the release cycle can be hidden when displaying the product page (mandatory)
# - is_lts (in cycles) : whether the release cycle is currently in its LTS phase (mandatory)
# - lts_from (in cycles) : the LTS phase start date for the release cycle (optional, only if lts is a Date)
# - is_eoas (in cycles) : whether the release cycle has reach the end of active support (optional, only if eoas is set)
# - is_almost_eoas (in cycles) : whether the release cycle will soon reach the end of active support (optional, only if eoas is set to a Date)
# - eoas_from (in cycles) : end of the release cycle active support phase date (optional, only if eoas is set to a Date)
# - is_eol (in cycles) : whether the release cycle is currently eol (optional, only if eol is set)
# - is_almost_eol (in cycles) : whether the release cycle will soon reach eol (optional, only if eol is set to a Date)
# - eol_from (in cycles) : EOL date of the release cycle (optional, only if eol is set to a Date)
# - is_discontinued (in cycles) : whether the release cycle is currently discontinued (optional, only if discontinued is set)
# - is_almost_discontinued (in cycles) : whether the release cycle will soon be discontinued (optional, only if discontinued is set to a Date)
# - discontinued_from (in cycles) : discontinuation date of the release cycle (optional, only if discontinued is set to a Date)
# - is_eoes (in cycles) : whether the release cycle has reach the end of extended support (optional, only if eoes is set)
# - is_almost_eoes (in cycles) : whether the release cycle will soon reach the end of extended support (optional, only if eoes is set to a Date)
# - eoes_from (in cycles) : end of the release cycle extended support phase date (optional, only if eoes is set to a Date)

require_relative 'end-of-life'
require_relative 'identifier-to-url'

module Jekyll
  class ProductDataEnricher
    class << self

      TOPIC = "EndOfLife Product Data Enricher:"

      def enrich(page)
        Jekyll.logger.debug TOPIC, "Enriching #{page.name}"

        set_id(page)
        set_description(page)
        set_icon_url(page)
        set_parent(page)
        set_tags(page)
        set_identifiers_url(page)
        set_aliases(page)
        set_overridden_columns_label(page)

        page.data["releases"].each { |release| enrich_release(page, release) }

        # DO NOT MOVE : below methods need information computed by enrich_release.
        flag_oldest_unmaintained_releases(page)
      end

      def is_product?(page)
        page.data['layout'] == 'product'
      end

      private

      # Build the product id from the permalink.
      def set_id(page)
        page.data['id'] = page.data['permalink'][1..page.data['permalink'].length]
      end

      # Build the product description, if it's not already set in the product's front matter.
      def set_description(page)
        unless page.data['description']
          page.data['description'] = "Check end-of-life, release policy and support schedule for #{page.data['title']}."
        end
      end

      # Build the icon URL from the icon slug.
      def set_icon_url(page)
        if page['iconSlug']
          page.data['iconUrl'] = "https://cdn.jsdelivr.net/npm/simple-icons/icons/#{page['iconSlug']}.svg"
        end
      end

      # Set the parent page for navigation.
      def set_parent(page)
        page.data['parent'] = tag_title(page.data['category'])
      end

      # Explode tags space-separated string to a list if necessary.
      # Also add the category as a default tag.
      def set_tags(page)
        tags = page.data['tags']

        if tags
          tags = (tags.kind_of?(Array) ? tags : tags.split)
        else
          tags = []
        end

        tags << page.data['category']
        page.data['tags'] = tags.sort
      end

      # Set alias (derived from alternate_urls).
      def set_aliases(page)
        if page.data['alternate_urls']
          page.data['aliases'] = page.data['alternate_urls'].map { |path| path[1..] }
        else
          page.data['alternate_urls'] = [] # should be in a separate method, but easier that way
          page.data['aliases'] = []
        end
      end

      # Set each identifiers URL.
      def set_identifiers_url(page)
        for identifier in page.data['identifiers']
          unless identifier['url']
            identifier['url'] = IdentifierToUrl.new.render(identifier)
          end
        end
      end

      # Set properly the column presence/label if it was overridden.
      def set_overridden_columns_label(page)
        date_column_names = %w[releaseDateColumn latestColumn discontinuedColumn eoasColumn eolColumn eoesColumn]
        date_column_names.each { |date_column|
          if page.data[date_column].is_a? String
            page.data[date_column + 'Label'] = page.data[date_column]
            page.data[date_column] = true
          end
        }
      end

      # Flag all cycles that can be hidden (see #50).
      #
      # The goal of this function is to hide only a single run of rows, at the very end, if they are
      # all unmaintained. This function presume that all cycles are ordered by their release date,
      # so a cycle can be hidden only if:
      # - it is not the first cycle,
      # - it is unmaintained (see set_is_maintained below),
      # - the previous cycle is still maintained,
      # - all next cycles are unmaintained.
      #
      # This function applies only if there are more than 6 cycles and more than 2 cycles that can
      # be hidden.
      #
      # For example, given there are 10 cycles with various state of maintainability:
      # - cycle 1 to 3 are maintained => cannot be hidden because they are maintained.
      # - cycle 4 is unmaintained => cannot be hidden because cycle 5 is maintained.
      # - cycle 5 is maintained => cannot be hidden because it is maintained.
      # - cycle 6 is unmaintained => cannot be hidden because cycle 5 is maintained.
      # - cycle 7 to 10 are unmaintained => can be hidden.
      def flag_oldest_unmaintained_releases(page)
        min_total_cycles = 6 # apply only if the number of cycles is greater than this
        min_hidden_cycles = 3 # apply only if the number of hidden cycles is greater than this (must be < min_total_cycles)

        releases = page.data['releases']
        if releases.length <= min_total_cycles
          Jekyll.logger.debug TOPIC, "Less than #{min_total_cycles} cycles on #{page.name}, will not try to hide cycles"
          return
        end

        hidden_cycles = mark_cycles_that_can_be_hidden(releases)

        if releases[0]['can_be_hidden']
          Jekyll.logger.debug TOPIC, "First cycle is hidden on #{page.name}, unhide cycle"
          releases[0].delete('can_be_hidden')
          hidden_cycles.delete(releases[0])
        end

        if hidden_cycles.length > 0 and hidden_cycles.length < min_hidden_cycles
          Jekyll.logger.debug TOPIC, "Less than #{min_hidden_cycles} hidden cycles on #{page.name}, unhide #{hidden_cycles.length} cycles"
          hidden_cycles.each { |cycle| cycle.delete('can_be_hidden') }
          hidden_cycles.clear
        end

        Jekyll.logger.debug TOPIC, "Hide #{hidden_cycles.length} cycles on #{page.name}"
      end

      def mark_cycles_that_can_be_hidden(ordered_by_date_desc_releases)
        hidden_cycles = []
        previous_cycle = nil

        ordered_by_date_desc_releases.reverse.each { |cycle|
          if not cycle['is_maintained']
            if previous_cycle
              previous_cycle['can_be_hidden'] = true
              hidden_cycles << previous_cycle
            end

            previous_cycle = cycle
          else
            break
          end
        }

        return hidden_cycles
      end

      def enrich_release(page, cycle)
        set_cycle_id(cycle)
        set_cycle_lts_fields(cycle)
        set_cycle_eoas_fields(cycle)
        set_cycle_eoes_fields(cycle)
        set_cycle_eol_fields(cycle)
        set_cycle_discontinued_fields(cycle)
        set_cycle_link(page, cycle)
        set_cycle_label(page, cycle)
        add_lts_label_to_cycle_label(page, cycle) # must be called after set_cycle_lts
        set_is_maintained(cycle) # must be called after set_cycle_*_fields
      end

      # Build the cycle id from the permalink.
      def set_cycle_id(cycle)
        cycle['id'] = cycle['releaseCycle'].tr('/', '-')
      end

      # Set lts to false if it has no value and explode it to is_lts (boolean) and lts_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_lts_fields(cycle)
        unless cycle.has_key?('lts')
          cycle['lts'] = false
        end

        explode_date_or_boolean_field(cycle, 'lts', 'is_lts', 'lts_from')
      end

      # Explode eoas to is_eoas (boolean) and eoas_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_eoas_fields(cycle)
        explode_date_or_boolean_field(cycle, 'eoas', 'is_eoas', 'eoas_from')
        compute_almost_field(cycle, 'eoas', 'is_almost_eoas')
      end

      # Explode eoes to is_eoes (boolean) and eoes_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_eoes_fields(cycle)
        explode_date_or_boolean_field(cycle, 'eoes', 'is_eoes', 'eoes_from')
        compute_almost_field(cycle, 'eoes', 'is_almost_eoes')
      end

      # Explode eol to is_eol (boolean) and eol_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_eol_fields(cycle)
        explode_date_or_boolean_field(cycle, 'eol', 'is_eol', 'eol_from')
        compute_almost_field(cycle, 'eol', 'is_almost_eol')
      end

      # Explode discontinued to is_discontinued (boolean) and discontinued_from (Date).
      # See explode_date_or_boolean_field(...) for more information.
      def set_cycle_discontinued_fields(cycle)
        explode_date_or_boolean_field(cycle, 'discontinued', 'is_discontinued', 'discontinued_from')
        compute_almost_field(cycle, 'discontinued', 'is_almost_discontinued')
      end

      # Some release cycle fields (field_name) can be either a date or a boolean.
      # This function create two additional variables, one of boolean type (boolean_field_name) and
      # the other of Date type (date_field_name) to simplify usages in templates or Jekyll plugins.
      #
      # The invert parameter must be set according to the date nature. If it's a start date
      # (example : the eol field) set it to false, if it's an end date (example : the eoas field)
      # set it to true.
      def explode_date_or_boolean_field(cycle, field_name, boolean_field_name, date_field_name)
        unless cycle.has_key?(field_name)
          return
        end

        value = cycle[field_name]
        if value.is_a?(Date)
          cycle[boolean_field_name] = (Date.today > value)
          cycle[date_field_name] = value
        else
          cycle[boolean_field_name] = value
          cycle[date_field_name] = nil
        end
      end

      # Compute the almost_field_name field.
      def compute_almost_field(cycle, field_name, almost_field_name)
        field_value = cycle[field_name]
        unless field_value.is_a?(Date)
          return
        end

        period_start = cycle['releaseDate'].to_time.to_i # release at midnight
        period_end = field_value.to_time.to_i # eoas/eol/eoes at midnight
        now = Date.today.to_time.to_i # today at midnight
        time_until_end = period_end - now

        max_threshold = 4 * 30 * 24 * 60 * 60 # 4 months in seconds
        threshold = [(period_end - period_start) / 3, max_threshold].min
        is_almost_at_end = (0..threshold).include?(time_until_end)

        cycle[almost_field_name] = is_almost_at_end
      end

      def set_cycle_link(page, cycle)
        if cycle.has_key?('link')
          # null link means no changelog template
          if cycle['link'] && cycle['link'].include?('__')
            cycle['link'] = render_eol_template(cycle['link'], cycle)
          end
        else
          if page['changelogTemplate']
            cycle['link'] = render_eol_template(page['changelogTemplate'], cycle)
          end
        end
      end

      def set_cycle_label(page, cycle)
        template = cycle['releaseLabel'] || page.data['releaseLabel']
        if template
          cycle['label'] = render_eol_template(template, cycle)
        else
          cycle['label'] = cycle['releaseCycle']
        end
      end

      def add_lts_label_to_cycle_label(page, cycle)
        lts_label = page.data['LTSLabel']

        if cycle['lts_from']
          if cycle['is_lts']
            cycle['label'] = "#{cycle['label']} (#{lts_label})"
          else
            cycle['label'] = "#{cycle['label']} (<span title=\"#{cycle['lts_from'].iso8601}\">Upcoming</span> #{lts_label})"
          end
        elsif cycle['is_lts']
          cycle['label'] = "#{cycle['label']} (#{lts_label})"
        end
      end

      # Compute whether the cycle is still maintained and add the result to the cycle's data.
      #
      # A cycle is maintained if at least one of the eoas / eol / discontinued / eoes dates
      # is in the future or is true.
      #
      # This function must be executed after all other field have been computed.
      def set_is_maintained(cycle)
        is_maintained = false

        %w[is_eoas is_eol is_discontinued is_eoes].each { |field|
          if cycle.has_key?(field) and not cycle[field]
            is_maintained = true
            break
          end
        }

        cycle['is_maintained'] = is_maintained
      end

      # Template rendering function that replaces placeholders.
      # The template is stripped to avoid unnecessary whitespaces in the output.
      def render_eol_template(template, cycle)
        link = template.strip().gsub('__RELEASE_CYCLE__', cycle['releaseCycle'] || '')
        link.gsub!('__CODENAME__', cycle['codename'] || '')
        link.gsub!('__RELEASE_DATE__', cycle['releaseDate'].iso8601)
        link.gsub!('__LATEST__', cycle['latest'] || '')
        link.gsub!('__LATEST_RELEASE_DATE__', cycle['latestReleaseDate'] ? cycle['latestReleaseDate'].iso8601 : '')
        return Liquid::Template.parse(link).render(@context)
      end
    end
  end
end

Jekyll::Hooks.register [:pages], :post_init, priority: Jekyll::Hooks::PRIORITY_MAP[:normal] do |page|
  Jekyll::ProductDataEnricher.enrich(page) if Jekyll::ProductDataEnricher.is_product?(page)
end


================================================
FILE: _plugins/product-data-validator.rb
================================================
# Verify product data by performing some validation before and after products are enriched.
# Note that the site build is stopped if the validation fails.
#
# The validation done before enrichment is the validation of the properties set by the users.
#
# The validation done after enrichment is mainly the validation of URLs, because most of the URLs
# are generated by the changelogTemplate. Note that this validation is not done by default because
# it takes a lot of time. You can activate it by setting the MUST_CHECK_URLS environment variable to
# true before building the site.

require 'jekyll'
require 'open-uri'
require_relative 'end-of-life'

module EndOfLifeHooks
  VERSION = '1.0.0'
  TOPIC = 'Product Validator:'
  VALID_CUSTOM_FIELD_DISPLAY = %w[none api-only after-release-column before-latest-column after-latest-column]

  IGNORED_URL_PREFIXES = {
    'https://www.nokia.com': 'always return a Net::ReadTimeout',
  }
  SUPPRESSED_BECAUSE_402 = 'may trigger a 402 Payment Required'
  SUPPRESSED_BECAUSE_403 = 'may trigger a 403 Forbidden or a redirection forbidden'
  SUPPRESSED_BECAUSE_404 = 'may trigger a 404 Not Found'
  SUPPRESSED_BECAUSE_502 = 'may return a 502 Bad Gateway'
  SUPPRESSED_BECAUSE_503 = 'may return a 503 Service Unavailable'
  SUPPRESSED_BECAUSE_CERT = 'site have an invalid certificate'
  SUPPRESSED_BECAUSE_CONN_FAILED = 'may fail when opening the TCP connection'
  SUPPRESSED_BECAUSE_EOF = 'may return an "unexpected eof while reading" error'
  SUPPRESSED_BECAUSE_TIMEOUT = 'may trigger an open or read timeout'
  SUPPRESSED_BECAUSE_UNAVAILABLE = 'site is temporary unavailable'
  SUPPRESSED_URL_PREFIXES = {
    'https://access.redhat.com/': SUPPRESSED_BECAUSE_403,
    'https://antixlinux.com': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://apex.oracle.com/sod': SUPPRESSED_BECAUSE_403,
    'https://arangodb.com': SUPPRESSED_BECAUSE_403,
    'https://area51.phpbb.com': SUPPRESSED_BECAUSE_403,
    'https://ark.intel.com': SUPPRESSED_BECAUSE_403,
    'https://azure.microsoft.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://business.adobe.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://blogs.oracle.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://blog.system76.com/post/': SUPPRESSED_BECAUSE_404,
    'https://codex.wordpress.org/Supported_Versions': SUPPRESSED_BECAUSE_EOF,
    'https://community.openvpn.net': SUPPRESSED_BECAUSE_403,
    'https://dev.mysql.com': SUPPRESSED_BECAUSE_403,
    'https://developer.apple.com': SUPPRESSED_BECAUSE_502,
    'https://developers.redhat.com': SUPPRESSED_BECAUSE_403,
    'https://docs.arangodb.com': SUPPRESSED_BECAUSE_404,
    'https://docs.clamav.net': SUPPRESSED_BECAUSE_403,
    'https://docs.couchdb.org': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://docs.gitlab.com': SUPPRESSED_BECAUSE_403,
    'https://docs.joomla.org': SUPPRESSED_BECAUSE_403,
    'https://docs-prv.pcisecuritystandards.org': SUPPRESSED_BECAUSE_403,
    'https://docs.rocket.chat': SUPPRESSED_BECAUSE_403,
    'https://dragonwell-jdk.io/': SUPPRESSED_BECAUSE_UNAVAILABLE,
    'https://docs-cortex.paloaltonetworks.com/': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://euro-linux.com': SUPPRESSED_BECAUSE_403,
    'https://ffmpeg.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://ftpdocs.broadcom.com/WebInterface/phpdocs/0/MSPSaccount/COMPAT/AllProdDates.HTML': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://forums.unrealircd.org': SUPPRESSED_BECAUSE_403,
    'https://github.com/angular/angular.js/blob': SUPPRESSED_BECAUSE_502,
    'https://github.com/ansible-community/ansible-build-data/blob/main/4/CHANGELOG-v4.rst': SUPPRESSED_BECAUSE_502,
    'https://github.com/hashicorp/consul/blob/v1.18.2/CHANGELOG.md': SUPPRESSED_BECAUSE_502,
    'https://github.com/hashicorp/consul/blob/v1.19.2/CHANGELOG.md': SUPPRESSED_BECAUSE_502,
    'https://github.com/hashicorp/consul/blob/v1.20.5/CHANGELOG.md': SUPPRESSED_BECAUSE_502,
    'https://github.com/nodejs/node/blob/main/doc/changelogs/': SUPPRESSED_BECAUSE_502,
    'https://helpx.adobe.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://investors.broadcom.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://jfrog.com/help/': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://kernelnewbies.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://make.wordpress.org': SUPPRESSED_BECAUSE_EOF,
    'https://mattermost.com': SUPPRESSED_BECAUSE_403,
    'https://mxlinux.org': SUPPRESSED_BECAUSE_403,
    'https://mirrors.slackware.com': SUPPRESSED_BECAUSE_403,
    'https://moodle.org/': SUPPRESSED_BECAUSE_403,
    'https://nextcloud.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://nuxt.com/docs/community/roadmap': SUPPRESSED_BECAUSE_404,
    'https://opensource.org/licenses/osl-3.0.php': SUPPRESSED_BECAUSE_403,
    'https://oxygenupdater.com/news/all/': SUPPRESSED_BECAUSE_403,
    'https://phabricator.wikimedia.org/T259771': SUPPRESSED_BECAUSE_403,
    'https://privatebin.info/': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://reload4j.qos.ch/': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://review.lineageos.org/': SUPPRESSED_BECAUSE_502,
    'https://search.maven.org': SUPPRESSED_BECAUSE_403,
    'https://stackoverflow.com': SUPPRESSED_BECAUSE_403,
    'https://support.azul.com': SUPPRESSED_BECAUSE_403,
    'https://support.citrix.com': SUPPRESSED_BECAUSE_403,
    'https://support.fairphone.com': SUPPRESSED_BECAUSE_403,
    'https://support.herodevs.com/hc/en-us/articles/': SUPPRESSED_BECAUSE_403,
    'https://support.microsoft.com': SUPPRESSED_BECAUSE_403,
    'https://twitter.com/OracleAPEX': SUPPRESSED_BECAUSE_403,
    'https://visualstudio.microsoft.com/': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://web.archive.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://webapps.bmc.com': SUPPRESSED_BECAUSE_403,
    'https://wiki.debian.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://wiki.mageia.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://wiki.mozilla.org/Release_Management/Calendar': SUPPRESSED_BECAUSE_403,
    'https://wiki.ubuntu.com': SUPPRESSED_BECAUSE_503,
    'https://wordpress.org': SUPPRESSED_BECAUSE_EOF,
    'https://www.akeneo.com/akeneo-pim-community-edition/': SUPPRESSED_BECAUSE_403,
    'https://www.amazon.com': SUPPRESSED_BECAUSE_403,
    'https://www.atlassian.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.adobe.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.betaarchive.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.blender.org': SUPPRESSED_BECAUSE_403,
    'https://www.centreon.com/centreon-editions/': SUPPRESSED_BECAUSE_503,
    'https://www.citrix.com/products/citrix-virtual-apps-and-desktops/': SUPPRESSED_BECAUSE_403,
    'https://www.clamav.net': SUPPRESSED_BECAUSE_403,
    'https://www.couchbase.com': SUPPRESSED_BECAUSE_403,
    'https://www.devuan.org': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://www.drupal.org/': SUPPRESSED_BECAUSE_403,
    'https://www.erlang.org/doc/system_principles/misc.html': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://www.hpe.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.ibm.com/support/pages/node/6451203': SUPPRESSED_BECAUSE_403,
    'https://www.intel.com': SUPPRESSED_BECAUSE_403,
    'https://www.java.com/releases/': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.mageia.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.mail-archive.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.microfocus.com/documentation/visual-cobol/': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.microsoft.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.mulesoft.com': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.mysql.com': SUPPRESSED_BECAUSE_403,
    'https://www.netapp.com/data-storage/ontap': SUPPRESSED_BECAUSE_403,
    'https://www.npmjs.com': SUPPRESSED_BECAUSE_403,
    'https://www.phpbb.com': SUPPRESSED_BECAUSE_403,
    'https://www.raspberrypi.com': SUPPRESSED_BECAUSE_403,
    'https://www.redmine.org': SUPPRESSED_BECAUSE_TIMEOUT,
    'https://www.reddit.com': SUPPRESSED_BECAUSE_403,
    'http://www.slackware.com': SUPPRESSED_BECAUSE_CONN_FAILED,
    'http://www.squid-cache.org/Versions/v6/squid-6.13-RELEASENOTES.html': SUPPRESSED_BECAUSE_CONN_FAILED,
    'https://www.techpowerup.com/gpuz/': SUPPRESSED_BECAUSE_403,
    'https://www.unrealircd.org/docs/UnrealIRCd_releases': SUPPRESSED_BECAUSE_403,
    'https://www.virtualbox.org': SUPPRESSED_BECAUSE_402,
    'https://www.zentyal.com': SUPPRESSED_BECAUSE_403,
  }
  USER_AGENT = 'Mozilla/5.0 (X11; Linux x86_64; rv:140.0) Gecko/20100101 Firefox/140.0'
  URL_CHECK_OPEN_TIMEOUT = 6
  URL_CHECK_TIMEOUT = 10
  URL_CHECK_MAX_RETRY = 3



  # Global error count
  @@error_count = 0

  def self.increase_error_count
    @@error_count += 1
  end

  def self.error_count
    @@error_count
  end

  def self.validate(product)
    start = Time.now
    Jekyll.logger.debug TOPIC, "Validating '#{product.name}'..."

    error_if = Validator.new('product', product, product.data)
    error_if.is_not_a_string('title')
    error_if.is_not_in('category', CATEGORIES)
    error_if.does_not_match('tags', /^[a-z0-9\-]+( [a-z0-9\-]+)*$/) if product.data.has_key?('tags')
    error_if.does_not_match('permalink', /^\/[a-z0-9-]+$/)
    error_if.does_not_match('alternate_urls', /^\/[a-z0-9\-_]+$/)
    error_if.is_not_a_string('versionCommand') if product.data.has_key?('versionCommand')
    error_if.is_not_an_url('releasePolicyLink') if product.data.has_key?('releasePolicyLink')
    error_if.is_not_an_url('releaseImage') if product.data.has_key?('releaseImage')
    error_if.is_not_an_url('changelogTemplate') if product.data.has_key?('changelogTemplate')
    error_if.is_not_a_string('releaseLabel') if product.data.has_key?('releaseLabel')
    error_if.is_not_a_string('LTSLabel')
    error_if.is_not_a_boolean_nor_a_string('eolColumn')
    error_if.is_not_a_boolean_nor_a_string('eoasColumn')
    error_if.is_not_a_boolean_nor_a_string('latestColumn')
    error_if.is_not_a_boolean_nor_a_string('releaseDateColumn')
    error_if.is_not_a_boolean_nor_a_string('discontinuedColumn')
    error_if.is_not_a_boolean_nor_a_string('eoesColumn')
    error_if.is_not_an_array('identifiers')
    error_if.is_not_an_array('releases')
    error_if.not_ordered_by_release_cycles('releases')
    error_if.undeclared_custom_field('releases')
    error_if.custom_field_type_is_not_string('releases')

    if product.data.has_key?('auto')
      error_if = Validator.new('auto', product, product.data['auto'])
      error_if.is_not_an_array('methods')
    end

    product.data['customFields'].each { |column|
      error_if = Validator.new('customFields', product, column)
      error_if.is_not_a_string('name')
      error_if.is_not_in('display', EndOfLifeHooks::VALID_CUSTOM_FIELD_DISPLAY)
      error_if.is_not_a_string('label')
      error_if.is_not_a_string('description') if column.has_key?('description')
      error_if.is_not_an_url('link') if column.has_key?('link')
    }

    release_names = product.data['releases'].map { |release| release['releaseCycle'] }
    release_name_duplicates = release_names.group_by { |name| name }.select { |_, count| count.size > 1 }.keys
    error_if.not_true(release_name_duplicates.length == 0, 'releases', release_name_duplicates, 'Duplicate releases')

    product.data['releases'].each { |release|
      error_if = Validator.new('releases', product, release)
      error_if.does_not_match('releaseCycle', /^[a-z0-9.\-+_]+$/)
      error_if.is_not_a_string('releaseLabel') if release.has_key?('releaseLabel')
      error_if.is_not_a_string('codename') if release.has_key?('codename')
      error_if.is_not_a_date('releaseDate')
      error_if.too_far_in_future('releaseDate')
      error_if.is_not_a_boolean_nor_a_date('eoas') if product.data['eoasColumn']
      error_if.is_not_a_boolean_nor_a_date('eol')
      error_if.is_not_a_boolean_nor_a_date('discontinued') if product.data['discontinuedColumn']
      error_if.is_not_a_boolean_nor_a_date('eoes') if product.data['eoesColumn'] and release.has_key?('eoes')
      error_if.is_not_a_boolean_nor_a_date('lts') if release.has_key?('lts')
      error_if.is_not_a_string('latest') if product.data['latestColumn']
      error_if.is_not_a_date('latestReleaseDate') if product.data['latestColumn'] and release.has_key?('latestReleaseDate')
      error_if.too_far_in_future('latestReleaseDate') if product.data['latestColumn'] and release.has_key?('latestReleaseDate')
      error_if.is_not_an_url('link') if release.has_key?('link') and release['link']

      error_if.is_not_before('releaseDate', 'eoas') if product.data['eoasColumn']
      error_if.is_not_before('releaseDate', 'eol')
      error_if.is_not_before('releaseDate', 'eoes') if product.data['eoesColumn']
      error_if.is_not_before('eoas', 'eol') if product.data['eoasColumn']
      error_if.is_not_before('eoas', 'eoes') if product.data['eoasColumn'] and product.data['eoesColumn']
      error_if.is_not_before('eol', 'eoes') if product.data['eoesColumn']
    }

    Jekyll.logger.debug TOPIC, "Product '#{product.name}' successfully validated in #{(Time.now - start).round(3)} seconds."
  end

  def self.validate_urls(product)
    if ENV.fetch('MUST_CHECK_URLS', false)
      start = Time.now
      Jekyll.logger.info TOPIC, "Validating urls for '#{product.name}'..."

      error_if = Validator.new('product', product, product.data)
      error_if.is_url_invalid('releasePolicyLink') if product.data['releasePolicyLink']
      error_if.is_url_invalid('releaseImage') if product.data['releaseImage']
      error_if.is_url_invalid('iconUrl') if product.data['iconUrl']
      error_if.contains_invalid_urls(product.content)

      product.data['customFields'].each { |field|
        error_if = Validator.new('customFields', product, field)
        error_if.is_url_invalid('link') if field['link']
      }

      product.data['identifiers'].each { |identifier|
        error_if = Validator.new('identifiers', product, identifier)
        error_if.is_url_invalid('url') if identifier['url']
      }

      product.data['releases'].each { |release|
        error_if = Validator.new('releases', product, release)
        error_if.is_url_invalid('link') if release['link']
      }

      Jekyll.logger.info TOPIC, "Product '#{product.name}' urls successfully validated in #{(Time.now - start).round(3)} seconds."
    end
  end

  private

  class Validator
    def initialize(name, product, data)
      @product = product
      @data = data
      @error_count = 0

      unless data.kind_of?(Hash)
        declare_error(name, data, "expecting an Hash, got #{data.class}")
        @data = {} # prevent further errors
      end
    end

    def error_count
      @error_count
    end

    def not_true(condition, property, value, details)
      unless condition
        declare_error(property, value, details)
      end
    end

    def is_not_an_array(property)
      value = @data[property]
      unless value.kind_of?(Array)
        declare_error(property, value, "expecting an Array, got #{value.class}")
      end
    end

    def is_not_in(property, valid_values)
      value = @data[property]
      unless valid_values.include?(value)
        declare_error(property, value, "expecting one of #{valid_values.join(', ')}")
      end
    end

    def does_not_match(property, regex)
      values = @data[property].kind_of?(Array) ? @data[property] : [@data[property]]
      values.each { |value|
        unless regex.match?(value)
          declare_error(property, value, "should match #{regex}")
        end
      }
    end

    def is_not_a_string(property)
      value = @data[property]
      unless value.kind_of?(String)
        declare_error(property, value, "expecting a value of type String, got #{value.class}")
      end
    end

    def is_not_an_url(property)
      does_not_match(property, /^https?:\/\/.+$/)
    end

    def is_not_a_date(property)
      value = @data[property]
      unless value.respond_to?(:strftime)
        declare_error(property, value, "expecting a value of type date, got #{value.class}")
      end
    end

    def too_far_in_future(property)
      value = @data[property]
      if value.respond_to?(:strftime) and value > Date.today + 7
        declare_error(property, value, "expecting a value in the next 7 days, got #{value}")
      end
    end

    def is_not_a_number(property)
      value = @data[property]
      unless value.kind_of?(Numeric)
        declare_error(property, value, "expecting a value of type numeric, got #{value.class}")
      end
    end

    def is_not_a_boolean_nor_a_date(property)
      value = @data[property]
      unless [true, false].include?(value) or value.respond_to?(:strftime)
        declare_error(property, value, "expecting a value of type boolean or date, got #{value.class}")
      end
    end

    def is_not_a_boolean_nor_a_string(property)
      value = @data[property]
      unless [true, false].include?(value) or value.kind_of?(String)
        declare_error(property, value, "expecting a value of type boolean or string, got #{value.class}")
      end
    end

    def is_not_before(property1, property2)
      value1 = @data[property1]
      value2 = @data[property2]

      if value1.respond_to?(:strftime) and value2.respond_to?(:strftime) and value1 > value2
        declare_error(property1, value1, "expecting a value before #{property2} (#{value2})")
      end
    end

    def not_ordered_by_release_cycles(property)
      releases = @data[property]

      previous_release_cycle = nil
      previous_release_date = nil
      releases.each do |release|
        next if release['outOfOrder']

        release_cycle = release['releaseCycle']
        release_date = release['releaseDate']

        if previous_release_date and previous_release_date < release_date
          declare_error(property, release_cycle, "expecting release (released on #{release_date}) to be before #{previous_release_cycle} (released on #{previous_release_date})")
        end

        previous_release_cycle = release_cycle
        previous_release_date = release_date
      end
    end

    def is_url_invalid(property)
      # strip is necessary because changelogTemplate is sometime reformatted on two lines by update-product-data.py
      url = @data[property].strip
      check_url(url)
    rescue => e
      declare_url_error(property, url, "got an error : '#{e}'")
    end

    # Retrieve all urls in the given markdown-formatted text and check them.
    def contains_invalid_urls(markdown)
      urls = markdown.scan(/]\((?<matching>http[^)"]+)/).flatten # matches [text](url) or [text](url "title")
      urls += markdown.scan(/<(?<matching>http[^>]+)/).flatten # matches <url>
      urls += markdown.scan(/: (?<matching>http[^"\n]+)/).flatten # matches [id]: url or [id]: url "title"
      urls.each do |url|
        begin
          check_url(url.strip) # strip url because matches on [text](url "title") end with a space
        rescue => e
          declare_url_error('content', url, "got an error : '#{e}'")
        end
      end
    end

    def undeclared_custom_field(property)
      releases = @data[property]

      standard_fields = %w[releaseCycle releaseLabel codename releaseDate eoas eol eoes discontinued latest latestReleaseDate link lts outOfOrder staleReleaseThresholdDays]
      custom_fields = @product["customFields"].map { |column| column["name"] }

      releases.each do |release|
        release_cycle = release['releaseCycle']
        release_fields = release.keys

        undeclared_fields = release_fields - standard_fields - custom_fields
        for field in undeclared_fields
          declare_error(field, release_cycle, "undeclared field")
        end
      end
    end

    def custom_field_type_is_not_string(property)
      releases = @data[property]

      custom_fields = @product["customFields"].map { |column| column["name"] }
      releases.each do |release|
        release_cycle = release['releaseCycle']

        for field in custom_fields
          value = release[field]
          # string values may be parsed as Date, but ultimately they are String
          if value != nil and !value.kind_of?(String) and !value.kind_of?(Date)
            declare_error(field, release_cycle, "expecting a value of type String or Date, got #{value.class}")
          end
        end
      end
    end

    def check_url(url)
      ignored_reason = is_ignored(url)
      if ignored_reason
        Jekyll.logger.warn TOPIC, "Ignore URL #{url} : #{ignored_reason}."
        return
      end

      retries = 0
      begin
        Jekyll.logger.debug TOPIC, "Checking URL #{url}..."
        URI.open(url, 'User-Agent' => USER_AGENT, :open_timeout => URL_CHECK_OPEN_TIMEOUT, :read_timeout => URL_CHECK_TIMEOUT) do |response|
          Jekyll.logger.debug TOPIC, "URL #{url} successfully checked, response code is #{response.status[0]}"
        end
      rescue OpenURI::HTTPError => e
        if e.io.status[0] == '429' && retries < URL_CHECK_MAX_RETRY
          retries += 1
          sleep_time = 2 ** retries
          Jekyll.logger.warn TOPIC, "Got a 429 (Too Many Requests) for URL #{url}, retrying in #{sleep_time} seconds..."
          sleep(sleep_time)
          retry
        else
          raise e
        end
      end
    end

    def is_ignored(url)
      EndOfLifeHooks::IGNORED_URL_PREFIXES.each do |ignored_url, reason|
        return reason if url.start_with?(ignored_url.to_s)
      end

      return nil
    end

    def is_suppressed(url)
      EndOfLifeHooks::SUPPRESSED_URL_PREFIXES.each do |ignored_url, reason|
        return reason if url.start_with?(ignored_url.to_s)
      end

      return nil
    end

    def declare_url_error(property, url, details)
      reason = is_suppressed(url)
      if reason
        Jekyll.logger.warn TOPIC, "Invalid #{property} '#{url}' for #{location}, #{details} (suppressed: #{reason})."
      else
        declare_error(property, url, details)
      end

    end

    def declare_error(property, value, details)
      Jekyll.logger.error TOPIC, "Invalid #{property} '#{value}' for #{location}, #{details}."
      EndOfLifeHooks::increase_error_count()
    end

    def location
      if @data.kind_of?(Hash) and @data.has_key?('releaseCycle')
        "#{@product.name}#releases##{@data['releaseCycle']}"
      elsif @data.kind_of?(Hash) and @data.has_key?('name')
        "#{@product.name}#customField##{@data['name']}"
      else
        @product.name
      end
    end
  end
end

class TooManyRequestsError < StandardError
  attr_reader :response
  def initialize(response)
    @response = response
    super("response code is 429 (Too Many Requests)")
  end
end

# Must be run before enrichment, hence the high priority.
Jekyll::Hooks.register :pages, :post_init, priority: Jekyll::Hooks::PRIORITY_MAP[:high] do |page, payload|
  if page.data['layout'] == 'product'
    EndOfLifeHooks::validate(page)
  end
end

# Must be run after enrichment, hence the low priority.
Jekyll::Hooks.register :pages, :post_init, priority: Jekyll::Hooks::PRIORITY_MAP[:low] do |page, payload|
  if page.data['layout'] == 'product'
    EndOfLifeHooks::validate_urls(page)
  end
end

# Must be run at the end of all validation
Jekyll::Hooks.register :site, :post_render, priority: Jekyll::Hooks::PRIORITY_MAP[:low] do |site, payload|
  if EndOfLifeHooks::error_count > 0
    raise "Site build canceled : #{EndOfLifeHooks::error_count} errors detected"
  end
end


================================================
FILE: _redirects
================================================
---
# Netlify _redirects template. Documentation can be found on https://docs.netlify.com/routing/redirects/.
#
# The _redirects file is included in _config.yml/include key otherwise Jekyll doesn't copy it to
# _site directory, where Netlify expects it.
#
# To create a new redirect, add an alternate_urls array in the page front-matter.

# Setting a layout forces Jekyll to render this file
layout: null
---
# Clients will try to access /favicon.ico, in some scenarios we don't want the file in our codebase,
# because the theme embeds it as a favicon, so instead set a redirect for these clients to a PNG file instead.
/favicon.ico                       /assets/favicon-32x32.png

# A few permanent redirects for removed pages / alternate URLs
/advise                            /recommendations
/advice                            /recommendations
/java                              /tags/java-distribution
/jdk                               /tags/java-distribution
/tags/api-gateway                  /tags/web-server
/tags/configuration-management     /
/tags/db                           /tags/database
/tags/library                      /tags/framework
/tags/managed-mysql                /tags/database
/tags/managed-postgresql           /tags/database
/tags/package-manager              /tags/build-tool
/tags/redhat                       /tags/red-hat

{%- assign product_pages = site.pages | where: 'layout', 'product' %}
{%- for page in product_pages %}

# Redirects for {{page.path}}
{{page.permalink}}/_edit                https://github.com/endoflife-date/endoflife.date/edit/master/{{page.path}}
/api{{page.permalink}}                  /api{{page.permalink}}.json
{%- for alternate_url in page.alternate_urls %}
{{alternate_url}}                       {{page.permalink}}
{{alternate_url}}.atom                  {{page.permalink}}.atom
/calendar{{alternate_url}}.ics          /calendar{{page.permalink}}.ics
/api{{alternate_url}}.json              /api{{page.permalink}}.json
/api{{alternate_url}}/*                 /api{{page.permalink}}/:splat
/api/v1/products{{alternate_url}}/*     /api/v1/products{{page.permalink}}/:splat
{%- endfor %}
{%- endfor %}

# Rewrite for /api/v1/ to keep URLs clean.
# All API responses are located in an index.json and must be accessible without the file name, such as:
# - /api/v1/index.json -> /api/v1/
# - /api/v1/products/almalinux/index.json -> /api/v1/products/almalinux/
# This uses shadowing : https://docs.netlify.com/routing/redirects/rewrites-proxies/#shadowing, and
# it must be declared at the end of the file to not take precedence on the redirects (see https://docs.netlify.com/routing/redirects/#rule-processing-order).
# This configuration prevents us from having 404 json responses when using the API.
# But declaring the necessary redirects manually would be hard to maintain (each time a new endpoint is added this would require additional redirect rules).
# So that's the best trade-off we can do for now.
/api/v1/*                               /api/v1/:splat/index.json                         200!


================================================
FILE: _sass/custom/custom.scss
================================================
// Overriding styles for some of our pages
// This gets compiled into the final CSS
// The file path comes from what the upstream allows:
// https://just-the-docs.github.io/just-the-docs/docs/customization/#override-and-completely-custom-styles

.bg-red-000, .bg-yellow-200, .bg-green-000, .bg-grey-lt-100, .site-footer {
  color: #1c1c1c;
}

a {
  color: #6c4dec;
}

.description {
  >blockquote {
    margin-left: 60px;
  }
  >p {
    &:nth-of-type(1) {
      clear: both;
    }
  }
  // Icons are 50x50, so this adds another 10 pixels
  min-height: 60px;
}

.product-logo {
  float: left;
  margin-right: .5em;
}

.bg-light {
  background-color: #f8f9fa !important;
}

.txt-linethrough {
  text-decoration: line-through;
}

.card {
  position: relative;
  display: -ms-flexbox;
  display: flex;
  -ms-flex-direction: column;
  flex-direction: column;
  min-width: 0;
  word-wrap: break-word;
  background-color: #fff;
  background-clip: border-box;
  border: 1px solid rgba(0, 0, 0, 0.125);
  border-radius: 0.25rem;
}

.card-body {
  -ms-flex: 1 1 auto;
  flex: 1 1 auto;
  min-height: 1px;
  padding: 1.25rem;
}

.align-items-center {
  align-items: center;
}

// Inject site logo before the site title
.site-title {
  margin-left: 2rem;

  &:before {
    content: '';
    background:url('/assets/logo-192x192.png');
    background-size:cover;
    position:absolute;
    width: 2rem;
    height: 2rem;
    margin-left: -2.1rem;
    margin-bottom: -.3rem;
  }
}

#version-command {
  overflow: scroll;
}


// Based on https://dev.to/alvaromontoro/create-a-tag-cloud-with-html-and-css-1e90
ul.tag-cloud {
  list-style: none;
  padding-left: 0;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: center;
  line-height: 2.5rem;

  li::before { content: none !important; } // override JtD CSS
  li:nth-child(2n+1) a { --color: #181; }
  li:nth-child(3n+1) a { --color: #33a; }
  li:nth-child(4n+1) a { --color: #c38; }

  --tag-size-xs: 2;
  --tag-size-s: 4;
  --tag-size-m: 6;
  --tag-size-l: 8;
  --tag-size-xl: 10;
  a[data-weight="1"] { --size: var(--tag-size-xs); }
  a[data-weight="2"] { --size: var(--tag-size-xs); }
  a[data-weight="3"] { --size: var(--tag-size-xs); }
  a[data-weight="4"] { --size: var(--tag-size-xs); }
  a[data-weight="5"] { --size: var(--tag-size-xs); }
  a[data-weight="6"] { --size: var(--tag-size-xs); }
  a[data-weight="7"] { --size: var(--tag-size-xs); }
  a[data-weight="8"] { --size: var(--tag-size-xs); }
  a[data-weight="9"] { --size: var(--tag-size-xs); }
  a[data-weight="10"] { --size: var(--tag-size-xs); }
  a[data-weight="11"] { --size: var(--tag-size-s); }
  a[data-weight="12"] { --size: var(--tag-size-s); }
  a[data-weight="13"] { --size: var(--tag-size-s); }
  a[data-weight="14"] { --size: var(--tag-size-s); }
  a[data-weight="15"] { --size: var(--tag-size-s); }
  a[data-weight="16"] { --size: var(--tag-size-s); }
  a[data-weight="17"] { --size: var(--tag-size-s); }
  a[data-weight="18"] { --size: var(--tag-size-s); }
  a[data-weight="19"] { --size: var(--tag-size-s); }
  a[data-weight="20"] { --size: var(--tag-size-s); }
  a[data-weight="21"] { --size: var(--tag-size-m); }
  a[data-weight="22"] { --size: var(--tag-size-m); }
  a[data-weight="23"] { --size: var(--tag-size-m); }
  a[data-weight="24"] { --size: var(--tag-size-m); }
  a[data-weight="25"] { --size: var(--tag-size-m); }
  a[data-weight="26"] { --size: var(--tag-size-m); }
  a[data-weight="27"] { --size: var(--tag-size-m); }
  a[data-weight="28"] { --size: var(--tag-size-m); }
  a[data-weight="29"] { --size: var(--tag-size-m); }
  a[data-weight="30"] { --size: var(--tag-size-m); }
  a[data-weight="31"] { --size: var(--tag-size-l); }
  a[data-weight="32"] { --size: var(--tag-size-l); }
  a[data-weight="33"] { --size: var(--tag-size-l); }
  a[data-weight="34"] { --size: var(--tag-size-l); }
  a[data-weight="35"] { --size: var(--tag-size-l); }
  a[data-weight="36"] { --size: var(--tag-size-l); }
  a[data-weight="37"] { --size: var(--tag-size-l); }
  a[data-weight="38"] { --size: var(--tag-size-l); }
  a[data-weight="39"] { --size: var(--tag-size-l); }
  a[data-weight="40"] { --size: var(--tag-size-l); }
  a[data-weight="41"] { --size: var(--tag-size-l); }
  a[data-weight="42"] { --size: var(--tag-size-l); }
  a[data-weight="43"] { --size: var(--tag-size-l); }
  a[data-weight="44"] { --size: var(--tag-size-l); }
  a[data-weight="45"] { --size: var(--tag-size-l); }
  a[data-weight="46"] { --size: var(--tag-size-l); }
  a[data-weight="47"] { --size: var(--tag-size-l); }
  a[data-weight="48"] { --size: var(--tag-size-l); }
  a[data-weight="49"] { --size: var(--tag-size-l); }
  a[data-weight="50"] { --size: var(--tag-size-l); }

  a {
    --size: var(--tag-size-l);
    color: var(--color);
    font-size: calc(var(--size) * 0.25rem + 0.5rem);
    display: block;
    padding: 0.125rem 0.25rem;
    text-decoration: none;
    position: relative;
  }
}


================================================
FILE: api_v1/openapi.yml
================================================
---
# API v1 description. See https://spec.openapis.org/oas/v3.1.0 for specification.
# Edit using https://editor.swagger.io/.

permalink: /docs/api/v1/openapi.yml
layout: null
---
openapi: 3.1.1

info:
  title: endoflife API
  # This version must be kept in sync with the version in _plugins/generate-api-v1.rb.
  version: "1.2.0"
  license:
    name: MIT License
    url: "https://github.com/endoflife-date/endoflife.date/blob/master/LICENSE"
  contact:
    name: endoflife.date team
    url: https://github.com/endoflife-date/endoflife.date
  description: >-
    endoflife.date documents EOL dates and support lifecycles for various products.
    The endoflife API allows users to discover and query for those products.

    ## General Notes

    ### API changelog

    A changelog of the endoflife.date API is available in the [endoflife.date repository](https://github.com/endoflife-date/endoflife.date/blob/master/CHANGELOG_API.md).

    ### Backward compatibility

    The endoflife.date API is designed to be backward compatible, meaning that existing clients should continue to work
    with new versions of the API as long as your integration:

    - keep using the same major API version (`/api/v1`),

    - follow the 301 redirects (products, categories or tags may occasionally be renamed),

    - allow for new fields to be added in API responses (we may add new fields to support new features),

    - allow for new values to be added in enumeration fields (such as the product's category).

    ### Undocumented attributes

    Some APIs may return more data than indicated in the documentation.
    Do not rely on this undocumented data, there is no guarantee about it.

    ### API return codes

    The endoflife.date API uses standard HTTP return codes.


    When making HTTP requests, you can check the success or failure status of your request by using the HTTP Status Codes (i.e. 200).
    You must n
Download .txt
gitextract_evhxaady/

├── .editorconfig
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── feature_request.md
│   │   ├── new_product_suggestion.md
│   │   └── report_incorrect_details.md
│   ├── config.yml
│   ├── copilot-instructions.md
│   ├── dependabot.yml
│   └── workflows/
│       ├── auto-merge-release-updates.yml
│       ├── check-links.yml
│       └── lint.yml
├── .gitignore
├── .gitmodules
├── .markdownlint.yaml
├── .prettierignore
├── .prettierrc
├── .ruby-version
├── .vacuumignore.yml
├── 404.html
├── CHANGELOG_API.md
├── CODE-OF-CONDUCT.md
├── CONTRIBUTING.md
├── Gemfile
├── HACKING.md
├── LICENSE
├── README.md
├── _config.yml
├── _headers
├── _includes/
│   ├── css/
│   │   └── activation.scss.liquid
│   ├── custom-column-td.html
│   ├── custom-column-th.html
│   ├── head_custom.html
│   ├── identifiers.html
│   ├── lunr/
│   │   ├── custom-data.json
│   │   └── custom-index.js
│   ├── nav_footer_custom.html
│   ├── product-icon.html
│   ├── table.html
│   └── variables.html
├── _layouts/
│   ├── json.json
│   ├── new-products-feed.atom
│   ├── page.html
│   ├── product-feed.atom
│   ├── product-list.html
│   ├── product-tags.html
│   ├── product.html
│   ├── schema.html
│   └── swagger-ui.html
├── _plugins/
│   ├── create-icalendar-files.rb
│   ├── end-of-life-filters.rb
│   ├── end-of-life.rb
│   ├── generate-api-v0.rb
│   ├── generate-api-v1.rb
│   ├── generate-product-feeds.rb
│   ├── generate-tag-pages.rb
│   ├── identifier-to-url.rb
│   ├── product-data-enricher.rb
│   └── product-data-validator.rb
├── _redirects
├── _sass/
│   └── custom/
│       └── custom.scss
├── api_v1/
│   ├── openapi.yml
│   └── swagger-ui.md
├── assets/
│   ├── main.scss
│   ├── openapi.yml
│   └── register-show-hidden-releases-handler.js
├── bin/
│   ├── deploy.sh
│   ├── lint-product.sh
│   └── update_added_at.sh
├── bot.md
├── browserconfig.xml
├── humans.txt
├── index.md
├── manifest.json
├── netlify.toml
├── package.json
├── pages/
│   └── help/
│       └── identifiers-needed.md
├── product-schema.json
├── products/
│   ├── adonisjs.md
│   ├── akeneo-pim.md
│   ├── alibaba-ack.md
│   ├── alibaba-dragonwell.md
│   ├── almalinux.md
│   ├── alpine-linux.md
│   ├── amazon-aurora-postgresql.md
│   ├── amazon-cdk.md
│   ├── amazon-corretto.md
│   ├── amazon-documentdb.md
│   ├── amazon-eks.md
│   ├── amazon-elasticache-redis.md
│   ├── amazon-glue.md
│   ├── amazon-linux.md
│   ├── amazon-msk.md
│   ├── amazon-neptune.md
│   ├── amazon-opensearch.md
│   ├── amazon-rds-mariadb.md
│   ├── amazon-rds-mysql.md
│   ├── amazon-rds-postgresql.md
│   ├── android.md
│   ├── angular.md
│   ├── angularjs.md
│   ├── ansible-core.md
│   ├── ansible.md
│   ├── anthropic-claude.md
│   ├── antix-linux.md
│   ├── apache-activemq-artemis.md
│   ├── apache-activemq.md
│   ├── apache-airflow.md
│   ├── apache-ant.md
│   ├── apache-apisix.md
│   ├── apache-camel.md
│   ├── apache-cassandra.md
│   ├── apache-couchdb.md
│   ├── apache-flink.md
│   ├── apache-groovy.md
│   ├── apache-hadoop.md
│   ├── apache-hop.md
│   ├── apache-http-server.md
│   ├── apache-kafka.md
│   ├── apache-lucene.md
│   ├── apache-maven.md
│   ├── apache-nifi.md
│   ├── apache-pulsar.md
│   ├── apache-spark.md
│   ├── apache-struts.md
│   ├── apache-subversion.md
│   ├── api-platform.md
│   ├── apple-tvos.md
│   ├── apple-watch.md
│   ├── arangodb.md
│   ├── argo-cd.md
│   ├── argo-workflows.md
│   ├── artifactory.md
│   ├── authentik.md
│   ├── aws-lambda.md
│   ├── azul-zulu.md
│   ├── azure-devops-server.md
│   ├── azure-kubernetes-service.md
│   ├── backdrop.md
│   ├── bamboo.md
│   ├── bazel.md
│   ├── beats.md
│   ├── behat.md
│   ├── bellsoft-liberica.md
│   ├── big-ip.md
│   ├── bigbluebutton.md
│   ├── bitbucket.md
│   ├── bitcoin-core.md
│   ├── blender.md
│   ├── bootstrap.md
│   ├── boundary.md
│   ├── bun.md
│   ├── cachet.md
│   ├── caddy.md
│   ├── cakephp.md
│   ├── calico.md
│   ├── centos-stream.md
│   ├── centos.md
│   ├── centreon.md
│   ├── cert-manager.md
│   ├── cfengine.md
│   ├── chef-infra-client.md
│   ├── chef-infra-server.md
│   ├── chef-inspec.md
│   ├── chef-supermarket.md
│   ├── chef-workstation.md
│   ├── chrome.md
│   ├── cilium.md
│   ├── cisco-ios-xe.md
│   ├── citrix-vad.md
│   ├── ckeditor.md
│   ├── clamav.md
│   ├── clearlinux.md
│   ├── cloud-sql-auth-proxy.md
│   ├── cnspec.md
│   ├── cockroachdb.md
│   ├── coder.md
│   ├── coldfusion.md
│   ├── commvault.md
│   ├── composer.md
│   ├── concrete-cms.md
│   ├── confluence.md
│   ├── consul.md
│   ├── containerd.md
│   ├── contao.md
│   ├── contour.md
│   ├── controlm.md
│   ├── cos.md
│   ├── couchbase-server.md
│   ├── craft-cms.md
│   ├── dbt-core.md
│   ├── dce.md
│   ├── debian.md
│   ├── deno.md
│   ├── dependency-track.md
│   ├── devuan.md
│   ├── discourse.md
│   ├── django.md
│   ├── docker-engine.md
│   ├── dotnet.md
│   ├── dotnetfx.md
│   ├── dovecot.md
│   ├── drupal.md
│   ├── drush.md
│   ├── duckdb.md
│   ├── eclipse-jetty.md
│   ├── eclipse-temurin.md
│   ├── elasticsearch.md
│   ├── electron.md
│   ├── elixir.md
│   ├── emberjs.md
│   ├── envoy.md
│   ├── erlang.md
│   ├── eslint.md
│   ├── etcd.md
│   ├── eurolinux.md
│   ├── exim.md
│   ├── express.md
│   ├── fairphone.md
│   ├── fedora.md
│   ├── ffmpeg.md
│   ├── filemaker.md
│   ├── firefox.md
│   ├── fluent-bit.md
│   ├── flux.md
│   ├── font-awesome.md
│   ├── foreman.md
│   ├── forgejo.md
│   ├── fortios.md
│   ├── freebsd.md
│   ├── freedesktop-sdk.md
│   ├── gatekeeper.md
│   ├── gerrit.md
│   ├── ghc.md
│   ├── gitlab.md
│   ├── gleam.md
│   ├── go.md
│   ├── goaccess.md
│   ├── godot.md
│   ├── google-kubernetes-engine.md
│   ├── google-nexus.md
│   ├── gorilla.md
│   ├── graalvm-ce.md
│   ├── gradle.md
│   ├── grafana-loki.md
│   ├── grafana.md
│   ├── grails.md
│   ├── graylog.md
│   ├── greenlight.md
│   ├── grumphp.md
│   ├── grunt.md
│   ├── gstreamer.md
│   ├── guzzle.md
│   ├── haproxy.md
│   ├── harbor.md
│   ├── hashicorp-packer.md
│   ├── hashicorp-vault.md
│   ├── hbase.md
│   ├── hibernate-orm.md
│   ├── ibm-aix.md
│   ├── ibm-db2.md
│   ├── ibm-i.md
│   ├── ibm-mq.md
│   ├── ibm-semeru.md
│   ├── icinga-web.md
│   ├── icinga.md
│   ├── idl.md
│   ├── influxdb.md
│   ├── intel-processors.md
│   ├── internet-explorer.md
│   ├── ionic.md
│   ├── ios.md
│   ├── ipad.md
│   ├── ipados.md
│   ├── iphone.md
│   ├── isc-dhcp.md
│   ├── istio.md
│   ├── jaeger.md
│   ├── jekyll.md
│   ├── jenkins.md
│   ├── jhipster.md
│   ├── jira-software.md
│   ├── joomla.md
│   ├── jquery-ui.md
│   ├── jquery.md
│   ├── jreleaser.md
│   ├── julia.md
│   ├── karpenter.md
│   ├── kde-plasma.md
│   ├── keda.md
│   ├── keycloak.md
│   ├── kibana.md
│   ├── kindle.md
│   ├── kirby.md
│   ├── knative.md
│   ├── kong-gateway.md
│   ├── kotlin.md
│   ├── kubernetes-csi-node-driver-registrar.md
│   ├── kubernetes-node-feature-discovery.md
│   ├── kubernetes.md
│   ├── kuma.md
│   ├── kyverno.md
│   ├── laravel.md
│   ├── ldap-account-manager.md
│   ├── libreoffice.md
│   ├── lineageos.md
│   ├── linux-kernel.md
│   ├── linuxmint.md
│   ├── liquibase.md
│   ├── log4j.md
│   ├── logstash.md
│   ├── longhorn.md
│   ├── looker.md
│   ├── lua.md
│   ├── macos.md
│   ├── mageia.md
│   ├── magento.md
│   ├── mandrel.md
│   ├── mariadb.md
│   ├── mastodon.md
│   ├── matomo.md
│   ├── mattermost.md
│   ├── mautic.md
│   ├── mediawiki.md
│   ├── meilisearch.md
│   ├── memcached.md
│   ├── micronaut.md
│   ├── microsoft-build-of-openjdk.md
│   ├── mongodb.md
│   ├── moodle.md
│   ├── motorola-mobility.md
│   ├── msexchange.md
│   ├── mssqlserver.md
│   ├── mule-runtime.md
│   ├── mxlinux.md
│   ├── mysql.md
│   ├── neo4j.md
│   ├── neos.md
│   ├── netapp-ontap.md
│   ├── netbackup-appliance-os.md
│   ├── netbsd.md
│   ├── nextcloud.md
│   ├── nextjs.md
│   ├── nexus.md
│   ├── nginx.md
│   ├── nix.md
│   ├── nixos.md
│   ├── nodejs.md
│   ├── nokia.md
│   ├── nomad.md
│   ├── notepad-plus-plus.md
│   ├── numpy.md
│   ├── nutanix-aos.md
│   ├── nutanix-files.md
│   ├── nutanix-prism.md
│   ├── nuxt.md
│   ├── nvidia-driver.md
│   ├── nvidia-gpu.md
│   ├── nvm.md
│   ├── office.md
│   ├── omnissa-horizon.md
│   ├── oneplus.md
│   ├── openbao.md
│   ├── openbsd.md
│   ├── openjdk-builds-from-oracle.md
│   ├── opensearch.md
│   ├── openssl.md
│   ├── opensuse.md
│   ├── opentofu.md
│   ├── openvpn.md
│   ├── openwrt.md
│   ├── openzfs.md
│   ├── opnsense.md
│   ├── oracle-apex.md
│   ├── oracle-database.md
│   ├── oracle-graalvm.md
│   ├── oracle-jdk.md
│   ├── oracle-linux.md
│   ├── oracle-solaris.md
│   ├── otobo.md
│   ├── ovirt.md
│   ├── pan-cortex-xdr.md
│   ├── pan-gp.md
│   ├── pan-os.md
│   ├── pci-dss.md
│   ├── perl.md
│   ├── phoenix-framework.md
│   ├── php.md
│   ├── phpbb.md
│   ├── phpmyadmin.md
│   ├── pigeonhole.md
│   ├── pixel-watch.md
│   ├── pixel.md
│   ├── plesk.md
│   ├── plone.md
│   ├── pnpm.md
│   ├── podman.md
│   ├── pop-os.md
│   ├── postfix.md
│   ├── postgresql.md
│   ├── postmarketos.md
│   ├── powershell.md
│   ├── privatebin.md
│   ├── proftpd.md
│   ├── prometheus.md
│   ├── protractor.md
│   ├── proxmox-ve.md
│   ├── puppet.md
│   ├── python.md
│   ├── qt.md
│   ├── quarkus-framework.md
│   ├── quasar.md
│   ├── rabbitmq.md
│   ├── rancher.md
│   ├── raspberry-pi.md
│   ├── react-native.md
│   ├── react.md
│   ├── red-hat-ansible-automation-platform.md
│   ├── red-hat-build-of-openjdk.md
│   ├── red-hat-jboss-eap.md
│   ├── red-hat-openshift.md
│   ├── red-hat-satellite.md
│   ├── redis.md
│   ├── redmine.md
│   ├── renovate.md
│   ├── rhel.md
│   ├── robo.md
│   ├── rocket-chat.md
│   ├── rocky-linux.md
│   ├── ros-2.md
│   ├── ros.md
│   ├── roundcube.md
│   ├── routeros.md
│   ├── rtpengine.md
│   ├── ruby-on-rails.md
│   ├── ruby.md
│   ├── rust.md
│   ├── salt.md
│   ├── samsung-galaxy-tab.md
│   ├── samsung-galaxy-watch.md
│   ├── samsung-mobile.md
│   ├── sapmachine.md
│   ├── scala.md
│   ├── sharepoint.md
│   ├── shopware.md
│   ├── silverstripe.md
│   ├── slackware.md
│   ├── sles.md
│   ├── sns-firmware.md
│   ├── sns-hardware.md
│   ├── sns-smc.md
│   ├── solr.md
│   ├── sonarqube-community.md
│   ├── sonarqube-server.md
│   ├── sony-xperia.md
│   ├── sourcegraph.md
│   ├── splunk.md
│   ├── spring-boot.md
│   ├── spring-framework.md
│   ├── sqlite.md
│   ├── squid.md
│   ├── statamic.md
│   ├── steamos.md
│   ├── surface.md
│   ├── suse-linux-micro.md
│   ├── suse-manager.md
│   ├── svelte.md
│   ├── symfony.md
│   ├── tails.md
│   ├── tailwind-css.md
│   ├── tarantool.md
│   ├── tarteaucitron.md
│   ├── telegraf.md
│   ├── teleport.md
│   ├── terraform.md
│   ├── thumbor.md
│   ├── tls.md
│   ├── tomcat.md
│   ├── traefik.md
│   ├── twig.md
│   ├── typo3.md
│   ├── ubuntu.md
│   ├── umbraco.md
│   ├── unity.md
│   ├── unrealircd.md
│   ├── valkey.md
│   ├── varnish.md
│   ├── veeam-backup-and-replication.md
│   ├── veeam-backup-for-microsoft-365.md
│   ├── veeam-one.md
│   ├── virtualbox.md
│   ├── visionos.md
│   ├── visual-cobol.md
│   ├── visual-studio.md
│   ├── vitess.md
│   ├── vmware-cloud-foundation.md
│   ├── vmware-esxi.md
│   ├── vmware-harbor-registry.md
│   ├── vmware-photon.md
│   ├── vmware-srm.md
│   ├── vmware-vcenter.md
│   ├── vue.md
│   ├── vuetify.md
│   ├── wagtail.md
│   ├── watchos.md
│   ├── weakforced.md
│   ├── weechat.md
│   ├── windows-embedded.md
│   ├── windows-nano-server.md
│   ├── windows-powershell.md
│   ├── windows-server-core.md
│   ├── windows-server.md
│   ├── windows.md
│   ├── wireshark.md
│   ├── wordpress.md
│   ├── xcp-ng.md
│   ├── yarn.md
│   ├── yocto.md
│   ├── youtrack.md
│   ├── zabbix.md
│   ├── zentyal.md
│   ├── zerto.md
│   └── zookeeper.md
├── recommendations.md
├── robots.txt
├── runtime.txt
├── schema.html
└── sitemap.xml
Download .txt
SYMBOL INDEX (177 symbols across 11 files)

FILE: _plugins/create-icalendar-files.rb
  function load_yaml (line 12) | def load_yaml(file)
  class Product (line 20) | class Product
    method initialize (line 23) | def initialize(markdown_file)
    method permalink (line 27) | def permalink
    method link (line 31) | def link
    method title (line 35) | def title
    method release_cycles (line 39) | def release_cycles
  function icalendar_filename (line 49) | def icalendar_filename(output_dir, name)
  function notification_message (line 54) | def notification_message(product, cycle, type)
  function process_product (line 70) | def process_product(product)
  function process_all_files (line 117) | def process_all_files()

FILE: _plugins/end-of-life-filters.rb
  type EndOfLifeFilter (line 7) | module EndOfLifeFilter
    function liquify (line 11) | def liquify(input)
    function parse_uri (line 27) | def parse_uri(uri_str, part='host')
    function extract_element (line 32) | def extract_element(html, element)
    function remove_first_element (line 44) | def remove_first_element(html, element)
    function drop_zero_patch (line 56) | def drop_zero_patch(input)
    function collapse_cycles (line 77) | def collapse_cycles(cycles, field, range_separator)
    function days_from_now (line 94) | def days_from_now(from)
    function end_color (line 112) | def end_color(input)

FILE: _plugins/end-of-life.rb
  function is_category? (line 5) | def is_category?(name)
  function tag_title (line 12) | def tag_title(tag_name)
  function category_index (line 28) | def category_index(category_name)

FILE: _plugins/generate-api-v0.rb
  function load_yaml (line 17) | def load_yaml(file)
  class Product (line 25) | class Product
    method initialize (line 28) | def initialize(markdown_file)
    method permalink (line 32) | def permalink
    method release_cycles (line 36) | def release_cycles
  function json_filename (line 63) | def json_filename(output_dir, name)
  function process_product (line 68) | def process_product(product)
  function process_all_files (line 83) | def process_all_files()

FILE: _plugins/generate-api-v1.rb
  type ApiV1 (line 21) | module ApiV1
    function strip_html (line 37) | def self.strip_html(input)
    function site_url (line 44) | def self.site_url(site, path)
    function api_url (line 48) | def self.api_url(site, path)
    class ApiGenerator (line 52) | class ApiGenerator < Jekyll::Generator
      method generate (line 58) | def generate(site)
      method add_index_page (line 75) | def add_index_page(site)
      method add_products_related_pages (line 83) | def add_products_related_pages(site, products)
      method add_all_products_page (line 94) | def add_all_products_page(site, products)
      method add_all_products_and_releases_page (line 98) | def add_all_products_and_releases_page(site, products)
      method add_product_page (line 102) | def add_product_page(site, product)
      method add_latest_release_page (line 106) | def add_latest_release_page(site, page)
      method add_release_page (line 111) | def add_release_page(site, page, release)
      method add_categories_related_pages (line 115) | def add_categories_related_pages(site, products)
      method products_by_category (line 124) | def products_by_category(products)
      method add_category_page (line 130) | def add_category_page(site, category, products)
      method add_all_categories_page (line 134) | def add_all_categories_page(site, categories)
      method add_tags_related_pages (line 140) | def add_tags_related_pages(site, products)
      method products_by_tag (line 149) | def products_by_tag(products)
      method add_tag_page (line 157) | def add_tag_page(site, tag, products)
      method add_all_tags_page (line 161) | def add_all_tags_page(site, tags)
      method add_identifiers_related_pages (line 167) | def add_identifiers_related_pages(site, products)
      method identifiers_by_type (line 176) | def identifiers_by_type(site, products)
      method add_all_identifier_types_page (line 192) | def add_all_identifier_types_page(site, types)
      method add_identifiers_for_type_page (line 198) | def add_identifiers_for_type_page(site, type, identifiers)
      method add_to_map (line 203) | def add_to_map(map, key, page)
    class JsonPage (line 212) | class JsonPage < Jekyll::Page
      method of_raw_data (line 216) | def of_raw_data(site, path, data, metadata = {})
      method of_products_summary (line 220) | def of_products_summary(site, path, products)
      method of_products_details (line 226) | def of_products_details(site, path, products)
      method of_product (line 232) | def of_product(site, product)
      method of_release (line 242) | def of_release(site, product, release, identifier = nil)
      method product_to_json (line 249) | def product_to_json(site, product)
      method product_summary_to_json (line 273) | def product_summary_to_json(site, product)
      method release_to_json (line 284) | def release_to_json(product, release)
      method custom_fields (line 335) | def custom_fields(product, release)
      method initialize (line 342) | def initialize(site, path, data, metadata)

FILE: _plugins/generate-product-feeds.rb
  type EndOfLife (line 5) | module EndOfLife
    function site_url (line 7) | def self.site_url(site, path)
    class ProductFeedsGenerator (line 11) | class ProductFeedsGenerator < Jekyll::Generator
      method generate (line 17) | def generate(site)
    class ProductFeed (line 32) | class ProductFeed < Jekyll::Page
      method initialize (line 33) | def initialize(site, product)
    class NewProductsFeed (line 105) | class NewProductsFeed < Jekyll::Page
      method initialize (line 106) | def initialize(site)

FILE: _plugins/generate-tag-pages.rb
  type EndOfLife (line 6) | module EndOfLife
    class ProductPagesGenerator (line 8) | class ProductPagesGenerator < Jekyll::Generator
      method generate (line 14) | def generate(site)
      method products_by_tag (line 30) | def products_by_tag(products)
      method add_to_map (line 38) | def add_to_map(map, key, page)
    class TagsPage (line 47) | class TagsPage < Jekyll::Page
      method initialize (line 48) | def initialize(site, products_by_tag)
    class TagPage (line 68) | class TagPage < Jekyll::Page
      method initialize (line 69) | def initialize(site, tag, products)

FILE: _plugins/identifier-to-url.rb
  class IdentifierToUrl (line 6) | class IdentifierToUrl
    method render (line 8) | def render(identifier_hash)
    method _build_repology_url (line 51) | def _build_repology_url(identifier)
    method _build_cargo_url (line 55) | def _build_cargo_url(purl)
    method _build_docker_url (line 59) | def _build_docker_url(purl)
    method _build_github_url (line 65) | def _build_github_url(purl)
    method _build_bitbucket_url (line 70) | def _build_bitbucket_url(purl)
    method _build_gitlab_url (line 75) | def _build_gitlab_url(purl)
    method _build_gem_url (line 80) | def _build_gem_url(purl)
    method _build_cran_url (line 84) | def _build_cran_url(purl)
    method _build_npm_url (line 88) | def _build_npm_url(purl)
    method _build_pypi_url (line 93) | def _build_pypi_url(purl)
    method _build_composer_url (line 97) | def _build_composer_url(purl)
    method _build_nuget_url (line 102) | def _build_nuget_url(purl)
    method _build_hackage_url (line 107) | def _build_hackage_url(purl)
    method _build_hex_url (line 111) | def _build_hex_url(purl)
    method _build_golang_url (line 115) | def _build_golang_url(purl)
    method _build_scoop_url (line 120) | def _build_scoop_url(purl)
    method _build_oci_url (line 124) | def _build_oci_url(purl)
    method _build_chocolatey_url (line 130) | def _build_chocolatey_url(purl)
    method _build_brew_url (line 134) | def _build_brew_url(purl)
    method _build_winget_url (line 138) | def _build_winget_url(purl)
    method _build_maven_url (line 142) | def _build_maven_url(purl)
    method _build_apk_url (line 147) | def _build_apk_url(purl)
    method _build_deb_url (line 163) | def _build_deb_url(purl)
    method _build_rpm_url (line 187) | def _build_rpm_url(purl)
    method _build_swid_url (line 203) | def _build_swid_url(purl)
    method _build_generic_url (line 207) | def _build_generic_url(purl)
    method _build_alpm_url (line 211) | def _build_alpm_url(purl)

FILE: _plugins/product-data-enricher.rb
  type Jekyll (line 30) | module Jekyll
    class ProductDataEnricher (line 31) | class ProductDataEnricher
      method enrich (line 36) | def enrich(page)
      method is_product? (line 54) | def is_product?(page)
      method set_id (line 61) | def set_id(page)
      method set_description (line 66) | def set_description(page)
      method set_icon_url (line 73) | def set_icon_url(page)
      method set_parent (line 80) | def set_parent(page)
      method set_tags (line 86) | def set_tags(page)
      method set_aliases (line 100) | def set_aliases(page)
      method set_identifiers_url (line 110) | def set_identifiers_url(page)
      method set_overridden_columns_label (line 119) | def set_overridden_columns_label(page)
      method flag_oldest_unmaintained_releases (line 148) | def flag_oldest_unmaintained_releases(page)
      method mark_cycles_that_can_be_hidden (line 175) | def mark_cycles_that_can_be_hidden(ordered_by_date_desc_releases)
      method enrich_release (line 195) | def enrich_release(page, cycle)
      method set_cycle_id (line 209) | def set_cycle_id(cycle)
      method set_cycle_lts_fields (line 215) | def set_cycle_lts_fields(cycle)
      method set_cycle_eoas_fields (line 225) | def set_cycle_eoas_fields(cycle)
      method set_cycle_eoes_fields (line 232) | def set_cycle_eoes_fields(cycle)
      method set_cycle_eol_fields (line 239) | def set_cycle_eol_fields(cycle)
      method set_cycle_discontinued_fields (line 246) | def set_cycle_discontinued_fields(cycle)
      method explode_date_or_boolean_field (line 258) | def explode_date_or_boolean_field(cycle, field_name, boolean_field_n...
      method compute_almost_field (line 274) | def compute_almost_field(cycle, field_name, almost_field_name)
      method set_cycle_link (line 292) | def set_cycle_link(page, cycle)
      method set_cycle_label (line 305) | def set_cycle_label(page, cycle)
      method add_lts_label_to_cycle_label (line 314) | def add_lts_label_to_cycle_label(page, cycle)
      method set_is_maintained (line 334) | def set_is_maintained(cycle)
      method render_eol_template (line 349) | def render_eol_template(template, cycle)

FILE: _plugins/product-data-validator.rb
  type EndOfLifeHooks (line 15) | module EndOfLifeHooks
    function increase_error_count (line 147) | def self.increase_error_count
    function error_count (line 151) | def self.error_count
    function validate (line 155) | def self.validate(product)
    function validate_urls (line 229) | def self.validate_urls(product)
    class Validator (line 261) | class Validator
      method initialize (line 262) | def initialize(name, product, data)
      method error_count (line 273) | def error_count
      method not_true (line 277) | def not_true(condition, property, value, details)
      method is_not_an_array (line 283) | def is_not_an_array(property)
      method is_not_in (line 290) | def is_not_in(property, valid_values)
      method does_not_match (line 297) | def does_not_match(property, regex)
      method is_not_a_string (line 306) | def is_not_a_string(property)
      method is_not_an_url (line 313) | def is_not_an_url(property)
      method is_not_a_date (line 317) | def is_not_a_date(property)
      method too_far_in_future (line 324) | def too_far_in_future(property)
      method is_not_a_number (line 331) | def is_not_a_number(property)
      method is_not_a_boolean_nor_a_date (line 338) | def is_not_a_boolean_nor_a_date(property)
      method is_not_a_boolean_nor_a_string (line 345) | def is_not_a_boolean_nor_a_string(property)
      method is_not_before (line 352) | def is_not_before(property1, property2)
      method not_ordered_by_release_cycles (line 361) | def not_ordered_by_release_cycles(property)
      method is_url_invalid (line 381) | def is_url_invalid(property)
      method contains_invalid_urls (line 390) | def contains_invalid_urls(markdown)
      method undeclared_custom_field (line 403) | def undeclared_custom_field(property)
      method custom_field_type_is_not_string (line 420) | def custom_field_type_is_not_string(property)
      method check_url (line 437) | def check_url(url)
      method is_ignored (line 463) | def is_ignored(url)
      method is_suppressed (line 471) | def is_suppressed(url)
      method declare_url_error (line 479) | def declare_url_error(property, url, details)
      method declare_error (line 489) | def declare_error(property, value, details)
      method location (line 494) | def location
  class TooManyRequestsError (line 506) | class TooManyRequestsError < StandardError
    method initialize (line 508) | def initialize(response)

FILE: assets/register-show-hidden-releases-handler.js
  function showHideOldReleases (line 1) | function showHideOldReleases(show) {
  function registerShowHiddenReleasesHandler (line 14) | function registerShowHiddenReleasesHandler() {
Condensed preview — 527 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,633K chars).
[
  {
    "path": ".editorconfig",
    "chars": 341,
    "preview": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at https://editorconfig."
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 55,
    "preview": "github: endoflife-date\nopen_collective: endoflife-date\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 568,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"\"\nlabels: \"enhancement\"\nassignees: \"\"\n---\n\n**I"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/new_product_suggestion.md",
    "chars": 738,
    "preview": "---\nname: New product suggestion\nabout: Suggest a new product for endoflife.date\ntitle: \"\"\nlabels: \"request\"\nassignees: "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/report_incorrect_details.md",
    "chars": 739,
    "preview": "---\nname: Report Incorrect Details\nabout: Report incorrect details of a product on endoflife.date\ntitle: \"\"\nlabels: \"bug"
  },
  {
    "path": ".github/config.yml",
    "chars": 1048,
    "preview": "# We use https://github.com/behaviorbot/welcome\n\n# Comment to be posted to on first time issues\nnewIssueWelcomeComment: "
  },
  {
    "path": ".github/copilot-instructions.md",
    "chars": 11509,
    "preview": "# endoflife.date Copilot Instructions\n\nThis is a Jekyll-based static site that tracks End of Life dates and support life"
  },
  {
    "path": ".github/dependabot.yml",
    "chars": 809,
    "preview": "# To get started with Dependabot version updates, you'll need to specify which\n# package ecosystems to update and where "
  },
  {
    "path": ".github/workflows/auto-merge-release-updates.yml",
    "chars": 2247,
    "preview": "name: Dependabot auto-merge release-updates\non: pull_request\n\n# Based on https://docs.github.com/en/code-security/depend"
  },
  {
    "path": ".github/workflows/check-links.yml",
    "chars": 704,
    "preview": "name: Check URLs\n\non:\n  workflow_dispatch: # Manually run workflow.\n  schedule:\n    - cron: \"0 0 * * 0\" # At 00:00 on Su"
  },
  {
    "path": ".github/workflows/lint.yml",
    "chars": 1026,
    "preview": "name: Lint\n\non:\n  workflow_dispatch: # Manually run workflow.\n  push:\n    branches: [\"master\"]\n  pull_request:\n    branc"
  },
  {
    "path": ".gitignore",
    "chars": 249,
    "preview": "_site\n.sass-cache\n.jekyll-metadata\nvendor/bundle\napi/\ncalendar/\n.idea/\n.jekyll-cache\n_data/gke.json\n.bundle/\n*.swp\n.vsco"
  },
  {
    "path": ".gitmodules",
    "chars": 133,
    "preview": "[submodule \"_data/release-data\"]\n\tpath = _data/release-data\n\turl = https://github.com/endoflife-date/release-data.git\n\tb"
  },
  {
    "path": ".markdownlint.yaml",
    "chars": 1156,
    "preview": "# See https://github.com/DavidAnson/markdownlint/blob/main/schema/.markdownlint.yaml.\n\n# Default state for all rules\ndef"
  },
  {
    "path": ".prettierignore",
    "chars": 64,
    "preview": ".idea\n_includes\n_layouts\n_sass\n_site\napi\ncalendar\nmanifest.json\n"
  },
  {
    "path": ".prettierrc",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": ".ruby-version",
    "chars": 6,
    "preview": "3.4.6\n"
  },
  {
    "path": ".vacuumignore.yml",
    "chars": 400,
    "preview": "# This file is used to ignore specific linting rules for the OpenAPI specification.\n\n# Ignore ambiguous path warnings fo"
  },
  {
    "path": "404.html",
    "chars": 765,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>Page not Found | endoflife.date</title>\n  <sty"
  },
  {
    "path": "CHANGELOG_API.md",
    "chars": 5222,
    "preview": "# endoflife.date API Changelog\n\n## API v1.2.0\n\n- Introduce a new `/identifiers/{identifier}` API ([#7361](https://github"
  },
  {
    "path": "CODE-OF-CONDUCT.md",
    "chars": 5493,
    "preview": "# Contributor Covenant Code of Conduct\n\n## Our Pledge\n\nWe as members, contributors, and leaders pledge to make participa"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 26125,
    "preview": "---\nlayout: page\nnav_exclude: true\ntitle: Contributing\ndescription: Some information on how to contribute to https://end"
  },
  {
    "path": "Gemfile",
    "chars": 919,
    "preview": "source \"https://rubygems.org\"\n\ngem \"jekyll\", \"~> 4.4.1\"\n\n# If you want to use GitHub Pages, remove the \"gem \"jekyll\"\" ab"
  },
  {
    "path": "HACKING.md",
    "chars": 7426,
    "preview": "- [Development](#development)\n- [Build](#build)\n- [File and Directory structure](#file-and-directory-structure)\n- [Autom"
  },
  {
    "path": "LICENSE",
    "chars": 1067,
    "preview": "Copyright 2020 endoflife.date contributors\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
  },
  {
    "path": "README.md",
    "chars": 3656,
    "preview": "# endoflife.date\n\n[![Netlify Status](https://api.netlify.com/api/v1/badges/92f7a2a9-3cca-4916-a75e-f9db4ec39d48/deploy-s"
  },
  {
    "path": "_config.yml",
    "chars": 3003,
    "preview": "---\n# Jekyll configuration - https://jekyllrb.com/docs/configuration/\n\n# Site settings\nurl: https://endoflife.date\ntitle"
  },
  {
    "path": "_headers",
    "chars": 4170,
    "preview": "---\n# Netlify _headers template. See syntax on https://docs.netlify.com/routing/headers/.\n#\n# This configuration sets de"
  },
  {
    "path": "_includes/css/activation.scss.liquid",
    "chars": 424,
    "preview": "/*\n * In Just the Docs v0.7.0, overriding _includes/css/activation.scss.liquid with an empty file\n * results in a backgr"
  },
  {
    "path": "_includes/custom-column-td.html",
    "chars": 499,
    "preview": "{%- comment %}\nRender a product custom column data cell (<td>).\n\nParameters:\n- release (mandatory): a product release cy"
  },
  {
    "path": "_includes/custom-column-th.html",
    "chars": 408,
    "preview": "{%- comment %}\nRender a product custom column header cell (<th>).\n\nParameters:\n- column (mandatory): the custom column d"
  },
  {
    "path": "_includes/head_custom.html",
    "chars": 1482,
    "preview": "{%- comment %}\nFavicons were generated from the SVG icons with https://realfavicongenerator.net.\n\nThe SVG favicon suppor"
  },
  {
    "path": "_includes/identifiers.html",
    "chars": 642,
    "preview": "{% if page.identifiers.size > 0 %}\n<details>\n  <summary>Show Product Identifiers</summary>\n  <ul>\n  {% for identifier_ha"
  },
  {
    "path": "_includes/lunr/custom-data.json",
    "chars": 942,
    "preview": "{%- capture newline %}\n{% endcapture -%}\n{%- assign identifiers = \"\" %}\n{% for identifier_hash in include.page.identifie"
  },
  {
    "path": "_includes/lunr/custom-index.js",
    "chars": 199,
    "preview": "const content_to_merge = [\n  docs[i].content,\n  docs[i].category,\n  docs[i].tags,\n  docs[i].alternate_urls,\n  docs[i].ic"
  },
  {
    "path": "_includes/nav_footer_custom.html",
    "chars": 80,
    "preview": "<a href=\"https://github.com/endoflife-date/endoflife.date/#credits\">Credits</a>\n"
  },
  {
    "path": "_includes/product-icon.html",
    "chars": 614,
    "preview": "{%- assign product_icon_url = include.product.iconUrl %}\n{%- assign product_icon_category = include.product.category %}\n"
  },
  {
    "path": "_includes/table.html",
    "chars": 2774,
    "preview": "{%- comment %}\nRender a table from the given rows.\n\nConsidering a table with N column and M rows, the equivalent Markdow"
  },
  {
    "path": "_includes/variables.html",
    "chars": 686,
    "preview": "{%- assign api = site.data.openapi -%}\n\n{%- capture title -%}\n  {%- if page.title -%}\n    {{- page.title | append: ' - '"
  },
  {
    "path": "_layouts/json.json",
    "chars": 26,
    "preview": "{{ page.data | jsonify }}\n"
  },
  {
    "path": "_layouts/new-products-feed.atom",
    "chars": 826,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\">\n  <id>{{ site.url }}/new-products.atom"
  },
  {
    "path": "_layouts/page.html",
    "chars": 568,
    "preview": "---\nlayout: default\n---\n\n{{content}}\n\n<script>\n// Automatically focus the #search-input element once it appears in the D"
  },
  {
    "path": "_layouts/product-feed.atom",
    "chars": 2190,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<feed xmlns=\"http://www.w3.org/2005/Atom\">\n  <id>{{ page.product_link }}</id>\n  <"
  },
  {
    "path": "_layouts/product-list.html",
    "chars": 2072,
    "preview": "---\nlayout: default\n---\n{%- if page.is_category %}\n<h1>{{ page.title }}</h1>\n{%- else %}\n<h1>Products tagged with '{{ pa"
  },
  {
    "path": "_layouts/product-tags.html",
    "chars": 390,
    "preview": "---\nlayout: default\n---\n<h1>{{ page.title }}</h1>\n\n<ul class=\"tag-cloud\" role=\"navigation\" aria-label=\"Product tag cloud"
  },
  {
    "path": "_layouts/product.html",
    "chars": 9083,
    "preview": "---\nlayout: default\n---\n\n<div class=\"product-title\">\n  <div class=\"d-flex flex-justify-between align-items-center\">\n    "
  },
  {
    "path": "_layouts/schema.html",
    "chars": 2023,
    "preview": "{%- include variables.html -%}\n<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"view"
  },
  {
    "path": "_layouts/swagger-ui.html",
    "chars": 811,
    "preview": "---\nlayout: null\n---\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n  <meta charset=\"UTF-8\">\n  <title>{{ page.title }}</title>\n"
  },
  {
    "path": "_plugins/create-icalendar-files.rb",
    "chars": 3506,
    "preview": "#!/usr/bin/env ruby\n\n# This script creates an calendar/[product].ics file\n# in each markdown source file, where [product"
  },
  {
    "path": "_plugins/end-of-life-filters.rb",
    "chars": 4399,
    "preview": "require 'nokogiri'\n\n# Various custom filters used by endoflife.date.\n#\n# All the filters has been gathered in the same m"
  },
  {
    "path": "_plugins/end-of-life.rb",
    "chars": 926,
    "preview": "# All categories on endoflife.date.\n# This also defines the order in which they appear in the navigation, so keep ordere"
  },
  {
    "path": "_plugins/generate-api-v0.rb",
    "chars": 2831,
    "preview": "#!/usr/bin/env ruby\n\n# This script creates an api/[product]/[version].json file for each releaseCycle\n# in each markdown"
  },
  {
    "path": "_plugins/generate-api-v1.rb",
    "chars": 12139,
    "preview": "# This script creates API files for version 1 of the endoflife.date API.\n#\n# There are multiples endpoints :\n#\n# - /api/"
  },
  {
    "path": "_plugins/generate-product-feeds.rb",
    "chars": 3701,
    "preview": "# This script creates product pages for the website.\n\nrequire 'jekyll'\n\nmodule EndOfLife\n\n  def self.site_url(site, path"
  },
  {
    "path": "_plugins/generate-tag-pages.rb",
    "chars": 2482,
    "preview": "# This script create the tag (and categories, because they are also tags) pages for the website.\n\nrequire 'jekyll'\nrequi"
  },
  {
    "path": "_plugins/identifier-to-url.rb",
    "chars": 7292,
    "preview": "require 'package_url'\nrequire 'pp'\nrequire 'jekyll'\n\n# Generate URLs for different package type, raising an error if the"
  },
  {
    "path": "_plugins/product-data-enricher.rb",
    "chars": 15202,
    "preview": "# This plugin enriches the product pages by setting or precomputing its metadata, so that it can be\n# easily consumed in"
  },
  {
    "path": "_plugins/product-data-validator.rb",
    "chars": 23274,
    "preview": "# Verify product data by performing some validation before and after products are enriched.\n# Note that the site build i"
  },
  {
    "path": "_redirects",
    "chars": 3068,
    "preview": "---\n# Netlify _redirects template. Documentation can be found on https://docs.netlify.com/routing/redirects/.\n#\n# The _r"
  },
  {
    "path": "_sass/custom/custom.scss",
    "chars": 4920,
    "preview": "// Overriding styles for some of our pages\n// This gets compiled into the final CSS\n// The file path comes from what the"
  },
  {
    "path": "api_v1/openapi.yml",
    "chars": 31740,
    "preview": "---\n# API v1 description. See https://spec.openapis.org/oas/v3.1.0 for specification.\n# Edit using https://editor.swagge"
  },
  {
    "path": "api_v1/swagger-ui.md",
    "chars": 143,
    "preview": "---\ntitle: EndOfLife API v1 Swagger UI\npermalink: /docs/api/v1/\nopenapi_yml: /docs/api/v1/openapi.yml\nlayout: swagger-ui"
  },
  {
    "path": "assets/main.scss",
    "chars": 0,
    "preview": ""
  },
  {
    "path": "assets/openapi.yml",
    "chars": 10840,
    "preview": "openapi: 3.1.0\ninfo:\n  title: endoflife.date\n  version: 0.0.1\n  summary: endoflife.date API\n  description: \"The endoflif"
  },
  {
    "path": "assets/register-show-hidden-releases-handler.js",
    "chars": 712,
    "preview": "function showHideOldReleases(show) {\n  const showMoreRow = document.getElementById(\"show-more-row\");\n  const releases = "
  },
  {
    "path": "bin/deploy.sh",
    "chars": 895,
    "preview": "#!/bin/bash -e\n\n# Display context information\necho \"Current directory: $PWD\"\necho \"Current commit: $(git rev-parse HEAD)"
  },
  {
    "path": "bin/lint-product.sh",
    "chars": 153,
    "preview": "#!/bin/bash -e\n\necho \"Lint '$1' using markdownlint-cli2\"\nnpx markdownlint-cli2@latest $1\n\necho \"Lint '$1' using prettier"
  },
  {
    "path": "bin/update_added_at.sh",
    "chars": 565,
    "preview": "#!/bin/bash\n\nfor file in \"${1:-products}\"/*.md; do\n  echo \"Processing $file...\"\n\n  # Get the first commit date (ISO form"
  },
  {
    "path": "bot.md",
    "chars": 2073,
    "preview": "---\nlayout: page\nnav_exclude: true\npermalink: /bot\ntitle: About the endoflife.date bot\ndescription: Some information abo"
  },
  {
    "path": "browserconfig.xml",
    "chars": 404,
    "preview": "---\n# See https://webmasters.stackexchange.com/q/131077\n\n# Setting a layout forces Jekyll to render this file.\nlayout: n"
  },
  {
    "path": "humans.txt",
    "chars": 458,
    "preview": "/* TEAM */\n\nTeam: https://github.com/orgs/endoflife-date/people\nContributors: https://github.com/endoflife-date/endoflif"
  },
  {
    "path": "index.md",
    "chars": 4459,
    "preview": "---\nlayout: page\nnav_exclude: true\nsearch_exclude: true\ntitle: Home\ndescription: Check end-of-life, support schedule, an"
  },
  {
    "path": "manifest.json",
    "chars": 668,
    "preview": "---\n# See https://web.dev/add-manifest/\n\n# Setting a layout forces Jekyll to render this file.\nlayout: null\n---\n{\n    \"n"
  },
  {
    "path": "netlify.toml",
    "chars": 208,
    "preview": "[build]\n  command = \"\"\"\n  ./bin/deploy.sh\n  \"\"\"\n  publish = \"_site/\"\n\n[context.deploy-preview]\n  command = \"\"\"\n  ./bin/d"
  },
  {
    "path": "package.json",
    "chars": 77,
    "preview": "{\n  \"devDependencies\": {\n    \"netlify-plugin-submit-sitemap\": \"^0.4.0\"\n  }\n}\n"
  },
  {
    "path": "pages/help/identifiers-needed.md",
    "chars": 2634,
    "preview": "---\nlayout: default\nnav_exclude: true\ntitle: \"Help: Identifiers Needed\"\ndescription: Help add more identifiers to the si"
  },
  {
    "path": "product-schema.json",
    "chars": 15652,
    "preview": "{\n  \"$schema\": \"http://json-schema.org/schema#\",\n  \"type\": \"object\",\n  \"properties\": {\n    \"title\": {\n      \"title\": \"Ti"
  },
  {
    "path": "products/adonisjs.md",
    "chars": 1295,
    "preview": "---\ntitle: AdonisJS\naddedAt: 2025-08-18\ncategory: framework\ntags: javascript-runtime\niconSlug: adonisjs\npermalink: /adon"
  },
  {
    "path": "products/akeneo-pim.md",
    "chars": 4824,
    "preview": "---\ntitle: Akeneo PIM\naddedAt: 2023-08-01\ncategory: server-app\ntags: php-runtime\npermalink: /akeneo-pim\nreleasePolicyLin"
  },
  {
    "path": "products/alibaba-ack.md",
    "chars": 3744,
    "preview": "---\ntitle: Alibaba ACK\naddedAt: 2025-03-18\ncategory: service\ntags: alibaba managed-kubernetes\niconSlug: alibabacloud\nper"
  },
  {
    "path": "products/alibaba-dragonwell.md",
    "chars": 4540,
    "preview": "---\ntitle: Alibaba Dragonwell\naddedAt: 2023-04-07\ncategory: lang\ntags: alibaba java-distribution\niconSlug: openjdk\nperma"
  },
  {
    "path": "products/almalinux.md",
    "chars": 4197,
    "preview": "---\ntitle: AlmaLinux OS\naddedAt: 2022-03-20\ncategory: os\ntags: linux-distribution\niconSlug: almalinux\npermalink: /almali"
  },
  {
    "path": "products/alpine-linux.md",
    "chars": 6636,
    "preview": "---\ntitle: Alpine Linux\naddedAt: 2019-05-27\ncategory: os\ntags: linux-distribution\niconSlug: alpinelinux\npermalink: /alpi"
  },
  {
    "path": "products/amazon-aurora-postgresql.md",
    "chars": 4044,
    "preview": "---\ntitle: Amazon Aurora PostgreSQL\naddedAt: 2026-01-10\ncategory: service\ntags: amazon database\niconSlug: amazonrds\nperm"
  },
  {
    "path": "products/amazon-cdk.md",
    "chars": 944,
    "preview": "---\ntitle: Amazon CDK\naddedAt: 2023-11-03\ncategory: framework\niconSlug: amazonaws\ntags: amazon\npermalink: /amazon-cdk\nch"
  },
  {
    "path": "products/amazon-corretto.md",
    "chars": 7516,
    "preview": "---\ntitle: Amazon Corretto\naddedAt: 2023-02-09\ncategory: lang\ntags: amazon java-distribution\niconSlug: openjdk\npermalink"
  },
  {
    "path": "products/amazon-documentdb.md",
    "chars": 1745,
    "preview": "---\ntitle: Amazon DocumentDB\naddedAt: 2025-08-18\ncategory: service\ntags: amazon database\niconSlug: amazondocumentdb\nperm"
  },
  {
    "path": "products/amazon-eks.md",
    "chars": 8855,
    "preview": "---\ntitle: Amazon EKS\naddedAt: 2021-07-25\ncategory: service\ntags: amazon managed-kubernetes\niconSlug: amazoneks\npermalin"
  },
  {
    "path": "products/amazon-elasticache-redis.md",
    "chars": 2331,
    "preview": "---\ntitle: Amazon ElastiCache for Redis OSS\naddedAt: 2026-02-28\ncategory: service\ntags: amazon database\niconSlug: amazon"
  },
  {
    "path": "products/amazon-glue.md",
    "chars": 3547,
    "preview": "---\ntitle: Amazon Glue\naddedAt: 2023-09-24\ncategory: service\ntags: amazon\niconSlug: amazonaws\npermalink: /amazon-glue\nre"
  },
  {
    "path": "products/amazon-linux.md",
    "chars": 8699,
    "preview": "---\ntitle: Amazon Linux\naddedAt: 2021-04-07\ncategory: os\ntags: amazon linux-distribution\niconSlug: amazonaws\npermalink: "
  },
  {
    "path": "products/amazon-msk.md",
    "chars": 3677,
    "preview": "---\ntitle: Amazon MSK\naddedAt: 2025-07-28\ncategory: service\ntags: amazon\niconSlug: amazonaws\npermalink: /amazon-msk\nalte"
  },
  {
    "path": "products/amazon-neptune.md",
    "chars": 8778,
    "preview": "---\ntitle: Amazon Neptune\naddedAt: 2023-07-01\ncategory: service\ntags: amazon\niconSlug: amazonaws\npermalink: /amazon-nept"
  },
  {
    "path": "products/amazon-opensearch.md",
    "chars": 2844,
    "preview": "---\ntitle: Amazon OpenSearch\naddedAt: 2026-03-09\ncategory: service\ntags: amazon database\niconSlug: aws\npermalink: /amazo"
  },
  {
    "path": "products/amazon-rds-mariadb.md",
    "chars": 3360,
    "preview": "---\ntitle: Amazon RDS for MariaDB\naddedAt: 2024-08-01\ncategory: service\ntags: amazon database\niconSlug: amazonrds\npermal"
  },
  {
    "path": "products/amazon-rds-mysql.md",
    "chars": 3504,
    "preview": "---\ntitle: Amazon RDS for MySQL\naddedAt: 2023-03-08\ncategory: service\ntags: amazon database\niconSlug: amazonrds\npermalin"
  },
  {
    "path": "products/amazon-rds-postgresql.md",
    "chars": 4689,
    "preview": "---\ntitle: Amazon RDS for PostgreSQL\naddedAt: 2023-03-30\ncategory: service\ntags: amazon database\niconSlug: amazonrds\nper"
  },
  {
    "path": "products/android.md",
    "chars": 7425,
    "preview": "---\ntitle: Android OS\naddedAt: 2020-10-02\ncategory: os\ntags: google\niconSlug: android\npermalink: /android\nalternate_urls"
  },
  {
    "path": "products/angular.md",
    "chars": 4684,
    "preview": "---\ntitle: Angular\naddedAt: 2021-08-19\ncategory: framework\ntags: google javascript-runtime herodevs\niconSlug: angular\npe"
  },
  {
    "path": "products/angularjs.md",
    "chars": 3258,
    "preview": "---\ntitle: AngularJS\naddedAt: 2023-08-14\ncategory: framework\ntags: discontinued google javascript-runtime herodevs\nperma"
  },
  {
    "path": "products/ansible-core.md",
    "chars": 6316,
    "preview": "---\ntitle: Ansible-core\naddedAt: 2022-10-12\ncategory: framework\ntags: python-runtime red-hat\niconSlug: ansible\npermalink"
  },
  {
    "path": "products/ansible.md",
    "chars": 7246,
    "preview": "---\ntitle: Ansible\naddedAt: 2021-08-15\ncategory: app\ntags: python-runtime red-hat\niconSlug: ansible\npermalink: /ansible\n"
  },
  {
    "path": "products/anthropic-claude.md",
    "chars": 8084,
    "preview": "---\ntitle: Anthropic Claude\naddedAt: 2026-03-10\ncategory: service\ntags: llm\niconSlug: anthropic\npermalink: /claude\nalter"
  },
  {
    "path": "products/antix-linux.md",
    "chars": 2825,
    "preview": "---\ntitle: antiX Linux\naddedAt: 2022-10-31\ncategory: os\ntags: linux-distribution\npermalink: /antix\nalternate_urls:\n  - /"
  },
  {
    "path": "products/apache-activemq-artemis.md",
    "chars": 8575,
    "preview": "---\ntitle: Apache ActiveMQ Artemis\naddedAt: 2025-08-16\ncategory: server-app\ntags: apache java-runtime\niconSlug: apache\np"
  },
  {
    "path": "products/apache-activemq.md",
    "chars": 7917,
    "preview": "---\ntitle: Apache ActiveMQ Classic\naddedAt: 2023-07-25\ncategory: server-app\ntags: apache java-runtime\niconSlug: apache\np"
  },
  {
    "path": "products/apache-airflow.md",
    "chars": 2751,
    "preview": "---\ntitle: Apache Airflow\naddedAt: 2022-08-22\ncategory: framework\ntags: apache python-runtime\niconSlug: apacheairflow\npe"
  },
  {
    "path": "products/apache-ant.md",
    "chars": 1393,
    "preview": "---\ntitle: Apache Ant\naddedAt: 2025-02-18\ncategory: app\ntags: apache build-tool java-runtime\niconSlug: apacheant\npermali"
  },
  {
    "path": "products/apache-apisix.md",
    "chars": 4054,
    "preview": "---\ntitle: Apache APISIX\naddedAt: 2024-08-04\ncategory: server-app\ntags: apache web-server\niconSlug: apache\npermalink: /a"
  },
  {
    "path": "products/apache-camel.md",
    "chars": 9272,
    "preview": "---\ntitle: Apache Camel\naddedAt: 2023-01-26\ncategory: framework\ntags: apache java-runtime\npermalink: /apache-camel\nalter"
  },
  {
    "path": "products/apache-cassandra.md",
    "chars": 3132,
    "preview": "---\ntitle: Apache Cassandra\naddedAt: 2022-10-17\ncategory: database\ntags: apache java-runtime\niconSlug: apachecassandra\np"
  },
  {
    "path": "products/apache-couchdb.md",
    "chars": 1786,
    "preview": "---\ntitle: Apache CouchDB\naddedAt: 2024-08-10\ncategory: database\ntags: apache erlang-runtime\niconSlug: apachecouchdb\nper"
  },
  {
    "path": "products/apache-flink.md",
    "chars": 3052,
    "preview": "---\ntitle: Apache Flink\naddedAt: 2024-05-20\ncategory: server-app\ntags: apache java-runtime\niconSlug: apacheflink\npermali"
  },
  {
    "path": "products/apache-groovy.md",
    "chars": 3301,
    "preview": "---\ntitle: Apache Groovy\naddedAt: 2022-11-26\ncategory: lang\ntags: apache java-runtime\niconSlug: apachegroovy\npermalink: "
  },
  {
    "path": "products/apache-hadoop.md",
    "chars": 4058,
    "preview": "---\ntitle: Apache Hadoop\naddedAt: 2023-09-24\ncategory: database\ntags: apache java-runtime\niconSlug: apachehadoop\npermali"
  },
  {
    "path": "products/apache-hop.md",
    "chars": 4863,
    "preview": "---\ntitle: Apache Hop\naddedAt: 2023-10-15\ncategory: app\ntags: apache java-runtime\niconSlug: apache\npermalink: /apache-ho"
  },
  {
    "path": "products/apache-http-server.md",
    "chars": 2298,
    "preview": "---\ntitle: Apache HTTP Server\naddedAt: 2022-01-05\ncategory: server-app\ntags: apache web-server\niconSlug: apache\npermalin"
  },
  {
    "path": "products/apache-kafka.md",
    "chars": 8131,
    "preview": "---\ntitle: Apache Kafka\naddedAt: 2023-05-24\ncategory: server-app\ntags: apache java-runtime\niconSlug: apachekafka\npermali"
  },
  {
    "path": "products/apache-lucene.md",
    "chars": 1211,
    "preview": "---\ntitle: Apache Lucene\naddedAt: 2024-07-05\ncategory: framework\ntags: apache java-runtime\niconSlug: apachelucene\npermal"
  },
  {
    "path": "products/apache-maven.md",
    "chars": 2731,
    "preview": "---\ntitle: Apache Maven\naddedAt: 2022-11-26\ncategory: app\ntags: apache build-tool java-runtime\niconSlug: apachemaven\nper"
  },
  {
    "path": "products/apache-nifi.md",
    "chars": 3339,
    "preview": "---\ntitle: Apache NiFi\naddedAt: 2025-03-01\ncategory: server-app\ntags: apache java-runtime\niconSlug: apachenifi\npermalink"
  },
  {
    "path": "products/apache-pulsar.md",
    "chars": 3916,
    "preview": "---\ntitle: Apache Pulsar\naddedAt: 2025-03-08\ncategory: server-app\ntags: apache java-runtime\niconSlug: apachepulsar\nperma"
  },
  {
    "path": "products/apache-spark.md",
    "chars": 4453,
    "preview": "---\ntitle: Apache Spark\naddedAt: 2023-09-24\ncategory: server-app # not sure if this is the best category\ntags: apache ja"
  },
  {
    "path": "products/apache-struts.md",
    "chars": 3087,
    "preview": "---\ntitle: Apache Struts\naddedAt: 2024-01-21\ncategory: framework\ntags: apache herodevs java-runtime\niconSlug: apache\nper"
  },
  {
    "path": "products/apache-subversion.md",
    "chars": 3364,
    "preview": "---\ntitle: Apache Subversion\naddedAt: 2024-08-09\ncategory: server-app\niconSlug: subversion\npermalink: /apache-subversion"
  },
  {
    "path": "products/api-platform.md",
    "chars": 3772,
    "preview": "---\ntitle: API Platform\naddedAt: 2022-02-17\ncategory: framework\ntags: php-runtime\npermalink: /api-platform\nversionComman"
  },
  {
    "path": "products/apple-tvos.md",
    "chars": 2693,
    "preview": "---\ntitle: Apple tvOS\naddedAt: 2025-01-12\ncategory: os\ntags: apple\niconSlug: apple\npermalink: /tvos\nalternate_urls:\n  - "
  },
  {
    "path": "products/apple-watch.md",
    "chars": 5233,
    "preview": "---\ntitle: Apple Watch\naddedAt: 2023-10-15\ncategory: device\ntags: apple watch\niconSlug: apple\npermalink: /apple-watch\nre"
  },
  {
    "path": "products/arangodb.md",
    "chars": 2865,
    "preview": "---\ntitle: ArangoDB\naddedAt: 2024-01-05\ncategory: database\niconSlug: arangodb\npermalink: /arangodb\nreleasePolicyLink: ht"
  },
  {
    "path": "products/argo-cd.md",
    "chars": 4826,
    "preview": "---\ntitle: Argo CD\naddedAt: 2023-08-06\ncategory: server-app\ntags: cncf linux-foundation\niconSlug: argo\npermalink: /argo-"
  },
  {
    "path": "products/argo-workflows.md",
    "chars": 1792,
    "preview": "---\ntitle: Argo Workflows\naddedAt: 2025-11-16\ncategory: server-app\ntags: cncf linux-foundation\niconSlug: argo\npermalink:"
  },
  {
    "path": "products/artifactory.md",
    "chars": 5324,
    "preview": "---\ntitle: Artifactory\naddedAt: 2023-07-09\ncategory: server-app\niconSlug: jfrog\npermalink: /artifactory\nchangelogTemplat"
  },
  {
    "path": "products/authentik.md",
    "chars": 2325,
    "preview": "---\ntitle: authentik\naddedAt: 2026-01-06\ncategory: server-app\niconSlug: authentik\npermalink: /authentik\nreleasePolicyLin"
  },
  {
    "path": "products/aws-lambda.md",
    "chars": 14517,
    "preview": "---\ntitle: AWS Lambda\naddedAt: 2023-11-23\ncategory: service\ntags: amazon\niconSlug: awslambda\npermalink: /aws-lambda\nrele"
  },
  {
    "path": "products/azul-zulu.md",
    "chars": 10279,
    "preview": "---\ntitle: Azul Zulu\naddedAt: 2023-03-11\ncategory: lang\ntags: azul java-distribution\niconSlug: openjdk\npermalink: /azul-"
  },
  {
    "path": "products/azure-devops-server.md",
    "chars": 5932,
    "preview": "---\ntitle: Azure DevOps Server\naddedAt: 2022-03-21\ncategory: server-app\ntags: microsoft\npermalink: /azure-devops-server\n"
  },
  {
    "path": "products/azure-kubernetes-service.md",
    "chars": 6460,
    "preview": "---\ntitle: Azure Kubernetes Service\naddedAt: 2022-12-28\ncategory: service\ntags: managed-kubernetes microsoft\npermalink: "
  },
  {
    "path": "products/backdrop.md",
    "chars": 1621,
    "preview": "---\ntitle: Backdrop\naddedAt: 2025-01-03\ncategory: server-app\ntags: php-runtime\npermalink: /backdrop\nreleasePolicyLink: h"
  },
  {
    "path": "products/bamboo.md",
    "chars": 3976,
    "preview": "---\ntitle: Bamboo\naddedAt: 2025-03-25\ncategory: server-app\ntags: atlassian java-runtime\niconSlug: bamboo\npermalink: /bam"
  },
  {
    "path": "products/bazel.md",
    "chars": 2419,
    "preview": "---\ntitle: Bazel\naddedAt: 2023-12-15\ncategory: app\ntags: google build-tool java-runtime\niconSlug: bazel\npermalink: /baze"
  },
  {
    "path": "products/beats.md",
    "chars": 4279,
    "preview": "---\ntitle: Elastic Beats\naddedAt: 2022-12-20\ncategory: server-app\ntags: elastic\niconSlug: beats\npermalink: /beats\naltern"
  },
  {
    "path": "products/behat.md",
    "chars": 1014,
    "preview": "---\ntitle: Behat\naddedAt: 2025-06-29\ncategory: framework\ntags: php-runtime\npermalink: /behat\nchangelogTemplate: https://"
  },
  {
    "path": "products/bellsoft-liberica.md",
    "chars": 19911,
    "preview": "---\ntitle: Bellsoft Liberica JDK\naddedAt: 2023-05-21\ncategory: lang\ntags: bellsoft java-distribution\niconSlug: openjdk\np"
  },
  {
    "path": "products/big-ip.md",
    "chars": 3655,
    "preview": "---\ntitle: BIG-IP\naddedAt: 2025-01-12\ncategory: os\niconSlug: f5\npermalink: /big-ip\nversionCommand: show /sys version\nrel"
  },
  {
    "path": "products/bigbluebutton.md",
    "chars": 1353,
    "preview": "---\ntitle: BigBlueButton\naddedAt: 2025-03-01\ncategory: server-app\niconSlug: bigbluebutton\npermalink: /bigbluebutton\nalte"
  },
  {
    "path": "products/bitbucket.md",
    "chars": 6482,
    "preview": "---\ntitle: Bitbucket\naddedAt: 2025-03-24\ncategory: server-app\ntags: atlassian java-runtime\niconSlug: bitbucket\npermalink"
  },
  {
    "path": "products/bitcoin-core.md",
    "chars": 4917,
    "preview": "---\ntitle: Bitcoin Core\naddedAt: 2025-08-15\ncategory: server-app\niconSlug: bitcoin\npermalink: /bitcoin-core\nreleasePolic"
  },
  {
    "path": "products/blender.md",
    "chars": 4724,
    "preview": "---\ntitle: Blender\naddedAt: 2021-09-03\ncategory: app\niconSlug: blender\npermalink: /blender\nversionCommand: blender --ver"
  },
  {
    "path": "products/bootstrap.md",
    "chars": 2382,
    "preview": "---\ntitle: Bootstrap\naddedAt: 2019-08-01\ncategory: framework\ntags: css-runtime javascript-runtime herodevs\niconSlug: boo"
  },
  {
    "path": "products/boundary.md",
    "chars": 2064,
    "preview": "---\ntitle: Hashicorp Boundary\naddedAt: 2025-08-04\ncategory: server-app\ntags: hashicorp\niconSlug: hashicorp\npermalink: /b"
  },
  {
    "path": "products/bun.md",
    "chars": 927,
    "preview": "---\ntitle: Bun\naddedAt: 2024-02-17\ncategory: framework\ntags: javascript-runtime\niconSlug: bun\npermalink: /bun\nversionCom"
  },
  {
    "path": "products/cachet.md",
    "chars": 2016,
    "preview": "---\ntitle: Cachet\naddedAt: 2025-08-18\ncategory: server-app\ntags: php-runtime\niconSlug: cachet\npermalink: /cachet\nchangel"
  },
  {
    "path": "products/caddy.md",
    "chars": 1383,
    "preview": "---\ntitle: Caddy\naddedAt: 2025-04-23\ncategory: server-app\ntags: web-server\niconSlug: caddy\npermalink: /caddy\nversionComm"
  },
  {
    "path": "products/cakephp.md",
    "chars": 9705,
    "preview": "---\ntitle: CakePHP\naddedAt: 2022-10-11\ncategory: framework\ntags: php-runtime\niconSlug: cakephp\npermalink: /cakephp\nalter"
  },
  {
    "path": "products/calico.md",
    "chars": 1868,
    "preview": "---\ntitle: Calico\naddedAt: 2024-07-13\ncategory: server-app\npermalink: /calico\nchangelogTemplate: https://github.com/proj"
  },
  {
    "path": "products/centos-stream.md",
    "chars": 1470,
    "preview": "---\ntitle: CentOS Stream\naddedAt: 2023-05-10\ncategory: os\ntags: linux-distribution\niconSlug: centos\npermalink: /centos-s"
  },
  {
    "path": "products/centos.md",
    "chars": 2439,
    "preview": "---\ntitle: CentOS\naddedAt: 2019-05-29\ncategory: os\ntags: discontinued linux-distribution\niconSlug: centos\npermalink: /ce"
  },
  {
    "path": "products/centreon.md",
    "chars": 4988,
    "preview": "---\ntitle: Centreon\naddedAt: 2024-03-17\ncategory: server-app\ntags: php-runtime\npermalink: /centreon\nreleasePolicyLink: h"
  },
  {
    "path": "products/cert-manager.md",
    "chars": 2435,
    "preview": "---\ntitle: cert-manager\naddedAt: 2023-12-15\ncategory: app\ntags: cncf linux-foundation\npermalink: /cert-manager\nversionCo"
  },
  {
    "path": "products/cfengine.md",
    "chars": 4097,
    "preview": "---\ntitle: CFEngine\naddedAt: 2021-11-01\ncategory: app\npermalink: /cfengine\nversionCommand: cf-agent --version\nreleasePol"
  },
  {
    "path": "products/chef-infra-client.md",
    "chars": 2967,
    "preview": "---\ntitle: Chef Infra Client\naddedAt: 2024-07-19\ncategory: app\ntags: progress ruby-runtime\niconSlug: chef\npermalink: /ch"
  },
  {
    "path": "products/chef-infra-server.md",
    "chars": 2556,
    "preview": "---\ntitle: Chef Infra Server\naddedAt: 2024-03-11\ncategory: server-app\ntags: erlang-runtime progress ruby-runtime\niconSlu"
  },
  {
    "path": "products/chef-inspec.md",
    "chars": 2435,
    "preview": "---\ntitle: Chef InSpec\naddedAt: 2024-08-14\ncategory: app\ntags: progress ruby-runtime\niconSlug: chef\npermalink: /chef-ins"
  },
  {
    "path": "products/chef-supermarket.md",
    "chars": 1841,
    "preview": "---\ntitle: Chef Supermarket\naddedAt: 2025-07-14\ncategory: server-app\ntags: progress ruby-runtime\niconSlug: chef\npermalin"
  },
  {
    "path": "products/chef-workstation.md",
    "chars": 2210,
    "preview": "---\ntitle: Chef Workstation\naddedAt: 2025-08-09\ncategory: app\ntags: progress ruby-runtime\niconSlug: chef\npermalink: /che"
  },
  {
    "path": "products/chrome.md",
    "chars": 13217,
    "preview": "---\ntitle: Google Chrome\naddedAt: 2025-07-15\ncategory: app\ntags: google web-browser\niconSlug: googlechrome\npermalink: /c"
  },
  {
    "path": "products/cilium.md",
    "chars": 1934,
    "preview": "---\ntitle: Cilium\naddedAt: 2025-12-14\ncategory: server-app\ntags: cncf linux-foundation\niconSlug: cilium\npermalink: /cili"
  },
  {
    "path": "products/cisco-ios-xe.md",
    "chars": 5504,
    "preview": "---\ntitle: Cisco IOS XE\naddedAt: 2025-11-08\ncategory: os\ntags: cisco\niconSlug: cisco\npermalink: /cisco-ios-xe\nreleasePol"
  },
  {
    "path": "products/citrix-vad.md",
    "chars": 8800,
    "preview": "---\ntitle: Citrix Virtual Apps and Desktops\naddedAt: 2021-12-16\ncategory: app\ntags: citrix\niconSlug: citrix\npermalink: /"
  },
  {
    "path": "products/ckeditor.md",
    "chars": 1245,
    "preview": "---\ntitle: CKEditor\naddedAt: 2024-02-20\ncategory: framework\niconSlug: ckeditor4\npermalink: /ckeditor\nreleasePolicyLink: "
  },
  {
    "path": "products/clamav.md",
    "chars": 3513,
    "preview": "---\ntitle: ClamAV\naddedAt: 2022-12-15\ncategory: app\npermalink: /clamav\nversionCommand: clamscan --version\nreleasePolicyL"
  },
  {
    "path": "products/clearlinux.md",
    "chars": 1134,
    "preview": "---\ntitle: Clear Linux\naddedAt: 2025-07-22\ncategory: os\ntags: discontinued linux-distribution intel\niconSlug: intel\nperm"
  },
  {
    "path": "products/cloud-sql-auth-proxy.md",
    "chars": 1569,
    "preview": "---\ntitle: Cloud SQL Auth Proxy\naddedAt: 2025-10-11\ncategory: app\ntags: google\niconSlug: googlecloud\npermalink: /cloud-s"
  },
  {
    "path": "products/cnspec.md",
    "chars": 2449,
    "preview": "---\ntitle: cnspec\naddedAt: 2025-01-04\ncategory: app\ntags: mondoo\npermalink: /cnspec\nversionCommand: cnspec version\nrelea"
  },
  {
    "path": "products/cockroachdb.md",
    "chars": 6795,
    "preview": "---\ntitle: CockroachDB\naddedAt: 2024-03-10\ncategory: database\niconSlug: cockroachlabs\npermalink: /cockroachdb\nalternate_"
  },
  {
    "path": "products/coder.md",
    "chars": 5248,
    "preview": "---\ntitle: Coder\naddedAt: 2024-08-02\ncategory: server-app\niconSlug: coder\npermalink: /coder\nversionCommand: coder versio"
  },
  {
    "path": "products/coldfusion.md",
    "chars": 4169,
    "preview": "---\ntitle: Adobe ColdFusion\naddedAt: 2021-08-30\ncategory: server-app\ntags: adobe\npermalink: /coldfusion\nversionCommand: "
  },
  {
    "path": "products/commvault.md",
    "chars": 2956,
    "preview": "---\ntitle: Commvault\naddedAt: 2025-10-11\ncategory: app\npermalink: /commvault\nalternate_urls:\n  - /commvault-backup\nrelea"
  },
  {
    "path": "products/composer.md",
    "chars": 2134,
    "preview": "---\ntitle: Composer\naddedAt: 2022-02-07\ncategory: app\ntags: php-runtime\niconSlug: composer\npermalink: /composer\nversionC"
  },
  {
    "path": "products/concrete-cms.md",
    "chars": 2200,
    "preview": "---\ntitle: Concrete CMS\naddedAt: 2026-03-07\ncategory: server-app\ntags: php-runtime\npermalink: /concrete-cms\nalternate_ur"
  },
  {
    "path": "products/confluence.md",
    "chars": 6383,
    "preview": "---\ntitle: Confluence\naddedAt: 2022-12-18\ncategory: server-app\ntags: atlassian java-runtime\niconSlug: confluence\npermali"
  },
  {
    "path": "products/consul.md",
    "chars": 3544,
    "preview": "---\ntitle: Hashicorp Consul\naddedAt: 2021-10-20\ncategory: server-app\ntags: hashicorp\niconSlug: consul\npermalink: /consul"
  },
  {
    "path": "products/containerd.md",
    "chars": 4160,
    "preview": "---\ntitle: containerd\naddedAt: 2024-03-10\ncategory: app\ntags: linux-foundation\niconSlug: containerd\npermalink: /containe"
  },
  {
    "path": "products/contao.md",
    "chars": 3113,
    "preview": "---\ntitle: Contao\naddedAt: 2022-12-08\ncategory: server-app\niconSlug: contao\npermalink: /contao\nreleasePolicyLink: https:"
  },
  {
    "path": "products/contour.md",
    "chars": 2804,
    "preview": "---\ntitle: Contour\naddedAt: 2024-06-18\ncategory: server-app\ntags: cncf kubernetes linux-foundation\npermalink: /contour\nr"
  },
  {
    "path": "products/controlm.md",
    "chars": 2834,
    "preview": "---\ntitle: Control-M\naddedAt: 2024-08-17\ncategory: server-app\niconSlug: bmcsoftware\npermalink: /controlm\nalternate_urls:"
  },
  {
    "path": "products/cos.md",
    "chars": 6301,
    "preview": "---\ntitle: Google Container-Optimized OS (COS)\naddedAt: 2022-11-14\ncategory: os\ntags: google\niconSlug: googlecloud\nperma"
  },
  {
    "path": "products/couchbase-server.md",
    "chars": 8807,
    "preview": "---\ntitle: Couchbase Server\naddedAt: 2021-10-18\ncategory: database\niconSlug: couchbase\npermalink: /couchbase-server\nalte"
  },
  {
    "path": "products/craft-cms.md",
    "chars": 1924,
    "preview": "---\ntitle: Craft CMS\naddedAt: 2023-06-10\ncategory: server-app\ntags: php-runtime\niconSlug: craftcms\npermalink: /craft-cms"
  },
  {
    "path": "products/dbt-core.md",
    "chars": 3381,
    "preview": "---\ntitle: dbt Core\naddedAt: 2023-10-20\ncategory: app\ntags: python-runtime\niconSlug: dbt\npermalink: /dbt-core\nalternate_"
  },
  {
    "path": "products/dce.md",
    "chars": 1394,
    "preview": "---\ntitle: DaoCloud Enterprise\naddedAt: 2024-07-02\ncategory: server-app\ntags: kubernetes\npermalink: /dce\nreleasePolicyLi"
  },
  {
    "path": "products/debian.md",
    "chars": 6677,
    "preview": "---\ntitle: Debian\naddedAt: 2019-05-29\ncategory: os\ntags: linux-distribution\niconSlug: debian\npermalink: /debian\nversionC"
  },
  {
    "path": "products/deno.md",
    "chars": 2788,
    "preview": "---\ntitle: Deno\naddedAt: 2025-02-16\ncategory: framework\ntags: javascript-runtime\niconSlug: deno\npermalink: /deno\nversion"
  },
  {
    "path": "products/dependency-track.md",
    "chars": 1951,
    "preview": "---\ntitle: Dependency-Track\naddedAt: 2023-06-09\ncategory: server-app\ntags: java-runtime\niconSlug: owasp\npermalink: /depe"
  },
  {
    "path": "products/devuan.md",
    "chars": 3231,
    "preview": "---\ntitle: Devuan\naddedAt: 2022-10-29\ncategory: os\ntags: linux-distribution\npermalink: /devuan\nversionCommand: cat /etc/"
  },
  {
    "path": "products/discourse.md",
    "chars": 1950,
    "preview": "---\ntitle: Discourse\naddedAt: 2026-03-07\ncategory: server-app\ntags: javascript-runtime ruby-runtime\niconSlug: discourse\n"
  },
  {
    "path": "products/django.md",
    "chars": 8210,
    "preview": "---\ntitle: Django\naddedAt: 2019-05-29\ncategory: framework\ntags: python-runtime\niconSlug: django\npermalink: /django\nversi"
  },
  {
    "path": "products/docker-engine.md",
    "chars": 5708,
    "preview": "---\ntitle: Docker Engine\naddedAt: 2021-11-01\ncategory: app\niconSlug: docker\npermalink: /docker-engine\nversionCommand: do"
  },
  {
    "path": "products/dotnet.md",
    "chars": 5068,
    "preview": "---\ntitle: Microsoft .NET\naddedAt: 2020-11-10\ncategory: framework\ntags: microsoft\niconSlug: dotnet\npermalink: /dotnet\nal"
  },
  {
    "path": "products/dotnetfx.md",
    "chars": 2650,
    "preview": "---\ntitle: Microsoft .NET Framework\naddedAt: 2020-11-10\ncategory: framework\ntags: microsoft\niconSlug: dotnet\npermalink: "
  }
]

// ... and 327 more files (download for full content)

About this extraction

This page contains the full source code of the endoflife-date/endoflife.date GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 527 files (2.4 MB), approximately 643.7k tokens, and a symbol index with 177 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!