diff --git a/CHANGELOG_API.md b/CHANGELOG_API.md
new file mode 100644
index 000000000000..420e68918691
--- /dev/null
+++ b/CHANGELOG_API.md
@@ -0,0 +1,56 @@
+## 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",
+- is versioned using the `api/v1` prefix, making it easier to implement non-backward-compatible
+ changes,
+- is generated using a Jekyll Generator (see https://jekyllrb.com/docs/plugins/generators/).
+
+API v1 resolves : #394, #759, #905, #2062, #2066, #2078, #2160, #2331, #2431, #2595. It also reverts
+#2425 due to incompatibilities in redirect rules.
+
+The API v0 is still generated to give time to users to migrate to API v1.
+
+### 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 to a JSON document. This made it possible to add endoflife-level data, such as the number of products.
+- Array elements have been changed from a simple string to a full JSON document. This made it possible to expose new data, such as product category and tags (#2062).
+
+### Changes in the "Product" endpoint
+
+- Path has been changed from `api/.json` to `api/v1/products//`.
+- Response has been changed from a simple array to a JSON document. This made it possible to expose product-level data, such as product category and tags (#2062).
+- Cycles data now always contain all the release cycles properties, even if they are null (example: `discontinued`, `latest`, `latestReleaseDate`, `support`...).
+
+### Changes in the "Cycle" endpoint
+
+- Path has been changed from `api//.json` to `api/v1/products//cycles//`.
+- Cycles data now always contain all the release cycles properties, even if they are null (example: `discontinued`, `latest`, `latestReleaseDate`, `support`...).
+- A special `/api/v1/products//cycles/latest/` cycle, containing the same data as the latest cycle, has been added (#2078).
+
+### 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/` - list categories used on endoflife.date
+- `/api/v1/categories/` - list products having the given category
+- `/api/v1/tags/` - list tags used on endoflife.date
+- `/api/v1/tags/` - list products having the given tag
+
+
+
+## API v0
+
+On 2023-03-02 the v0 endpoints were:
+
+- "All products" (`/api/all.json`) : Return a list of all products.
+- "Product" (`/api/{product}.json`) : Get EoL dates of all cycles for a given product.
+- "Cycle" (`/api/{product}/{cycle}.json`) : Details of a single release cycle of a given product.
diff --git a/HACKING.md b/HACKING.md
index fe54fdc52c51..deae201869d4 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -105,7 +105,13 @@ The API is just JSON files generated in the `api` directory by `_plugins/create-
### API Documentation
-The API Documentation is available at and is generated from an OpenAPI Specification file located at `_data/openapi.yml`. The documentation is rendered [Stoplight Elements](https://meta.stoplight.io/docs/elements/ZG9jOjMyNjU4OTY0-introduction-to-elements).
+The current API v1 documentation is available at 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 available at 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
diff --git a/README.md b/README.md
index fbff44bd7f2b..455ae74a3220 100644
--- a/README.md
+++ b/README.md
@@ -26,9 +26,8 @@ While participating in the project, you must abide by its [Code of Conduct](CODE
## API
-An API is available for integration with CI platforms.
-API documentation is available at https://endoflife.date/docs/api.
-The API is currently in Alpha, and breaking changes can happen.
+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
@@ -46,6 +45,8 @@ endoflife.date is relying on various amazing software and components :
- [Just the Docs](https://github.com/just-the-docs/just-the-docs), a documentation theme for Jekyll.
- [Stoplight Elements](https://stoplight.io/open-source/elements), a collection of UI components for
displaying beautiful developer documentation from any OpenAPI document.
+- [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.
- 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.
diff --git a/_config.yml b/_config.yml
index 3fee2a5da028..1e0b428ad7ac 100644
--- a/_config.yml
+++ b/_config.yml
@@ -40,7 +40,7 @@ aux_links:
source:
- https://github.com/endoflife-date/endoflife.date
api:
- - /docs/api
+ - /docs/api/v1/
jekyll_timeago:
# Use 2 terms in relative timestamps:
# [YES] x years, y months
diff --git a/_headers b/_headers
index 24cb2b006d9c..c1460af8cf63 100644
--- a/_headers
+++ b/_headers
@@ -56,6 +56,10 @@ layout: null
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/
{% else %}
diff --git a/_layouts/json.json b/_layouts/json.json
new file mode 100644
index 000000000000..8c98299c885e
--- /dev/null
+++ b/_layouts/json.json
@@ -0,0 +1 @@
+{{ page.data | jsonify }}
diff --git a/_layouts/product.html b/_layouts/product.html
index 1faccfa0a7bc..3d65184cba7b 100644
--- a/_layouts/product.html
+++ b/_layouts/product.html
@@ -170,8 +170,8 @@
- A JSON version of this page is available at /api{{page.permalink}}.json .
- See the API Documentation for more information.
+ A JSON version of this page is available at /api/v1/products{{page.permalink}}/ .
+ See the API Documentation for more information.
You can subscribe to the iCalendar feed at /calendar{{page.permalink}}.ics .
diff --git a/_layouts/swagger-ui.html b/_layouts/swagger-ui.html
new file mode 100644
index 000000000000..b75f0d38846e
--- /dev/null
+++ b/_layouts/swagger-ui.html
@@ -0,0 +1,31 @@
+---
+layout: null
+---
+
+
+
+
+ {{ page.title }}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/_plugins/generate-api-v1.rb b/_plugins/generate-api-v1.rb
index 2f93923af10f..3640e878f20b 100755
--- a/_plugins/generate-api-v1.rb
+++ b/_plugins/generate-api-v1.rb
@@ -1,89 +1,260 @@
-#!/usr/bin/env ruby
-
# This script creates API files for version 1 of the endoflife.date API.
#
-# There are three kind of generated files :
-# - all.json: contains the list of all the product names.
-# - .json: contains a given product data ()including releases data).
-# - /.json: contains a given product release data.
+# There are multiples endpoints :
+#
+# - /api/v1/ - list all major endpoints (those not requiring a parameter)
+# - /api/v1/products/ - list all products
+# - /api/v1/products// - get a single product details
+# - /api/v1/products//latest - get details on the latest cycle for the given product
+# - /api/v1/products// - get details on the given cycle for the given product
+# - /api/v1/categories/ - list categories used on endoflife.date
+# - /api/v1/categories/ - list products having the given category
+# - /api/v1/tags/ - list tags used on endoflife.date
+# - /api/v1/tags/ - list products having the given tag
+
-require 'fileutils'
-require 'json'
-require 'yaml'
-require 'date'
+require 'jekyll'
module ApiV1
- # This API path
- DIR = 'api/v1'
+ VERSION = '1.0.0'
+ MAJOR_VERSION = VERSION.split('.')[0]
- # Returns the path of a file inside the API namespace.
- def self.file(name, *args)
- File.join(DIR, name, *args)
+ STRIP_HTML_BLOCKS = Regexp.union(
+ //m,
+ //m,
+ //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
- # Holds information about a product.
- Product = Class.new do
- attr_accessor :data
-
- # Initializes the product with the given product's markdown file.
- # The markdown file is expected to contain a YAML front-matter with the appropriate properties.
- #
- # Copying the data makes it easier to process it.
- def initialize(data)
- @data = Hash.new
- # The product name is derived from the product's permalink (ex. /debian => debian).
- @data["name"] = data['permalink'][1..data['permalink'].length]
- @data["title"] = data['title']
- @data["category"] = data['category']
- @data["iconSlug"] = data['iconSlug']
- @data["permalink"] = data['permalink']
- @data["versionCommand"] = data['versionCommand']
- @data["auto"] = data.has_key? 'auto'
- @data["releasePolicyLink"] = data['releasePolicyLink']
- @data["releases"] = data['releases'].map do |release|
- release_data = Hash.new
- release_data["name"] = release['releaseCycle']
- release_data["codename"] = release['codename']
- release_data["releaseDate"] = release['releaseDate']
- release_data["support"] = release['support']
- release_data["eol"] = release['eol']
- release_data["discontinued"] = release['discontinued']
- release_data["lts"] = release['lts'] || false # lts is optional, make sure it always has a value
- release_data["latest"] = release['latest']
- release_data["latestReleaseDate"] = release['latestReleaseDate']
- release_data
+ 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)
+
+ 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/')}" },
+ ])
+ end
+
+ def add_products_related_pages(site, products)
+ add_all_products_page(site, products)
+
+ products.each do |page|
+ add_product_page(site, page)
+ add_latest_cycle_page(site, page)
+ page.data['releases'].each { |cycle| add_cycle_page(site, page, cycle) }
end
end
- def name
- data["name"]
+ def add_all_products_page(site, products)
+ site.pages << JsonPage.of_products(site, '/products/', products)
+ end
+
+ def add_product_page(site, product)
+ site.pages << JsonPage.of_product(site, product)
+ end
+
+ def add_latest_cycle_page(site, page)
+ latest = page.data['releases'][0]
+ site.pages << JsonPage.of_cycle(site, page, latest, 'latest')
+ end
+
+ def add_cycle_page(site, page, cycle)
+ site.pages << JsonPage.of_cycle(site, page, cycle)
+ 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(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(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_to_map(map, key, page)
+ if map.has_key? key
+ map[key] << page
+ else
+ map[key] = [page]
+ end
end
end
-end
-product_names = []
-FileUtils.mkdir_p(ApiV1::file('.'))
-
-Dir['products/*.md'].each do |file|
- # Load and prepare data
- raw_data = YAML.safe_load(File.open(file), permitted_classes: [Date])
- product = ApiV1::Product.new(raw_data)
- product_names.append(product.name)
-
- # Write /.json
- product_file = ApiV1::file("#{product.name}.json")
- File.open(product_file, 'w') { |f| f.puts product.data.to_json }
-
- # Write all //.json
- FileUtils.mkdir_p(ApiV1::file(product.name))
- product.data["releases"].each do |release|
- # Any / characters in the name are replaced with - to avoid file errors.
- release_file = ApiV1::file(product.name, "#{release['name'].to_s.tr('/', '-')}.json")
- File.open(release_file, 'w') { |f| f.puts release.to_json }
+ 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(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_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,
+ auto: product.data.has_key?('auto'),
+ }
+ new(site, path, data, meta)
+ end
+
+ def of_cycle(site, product, cycle, identifier = nil)
+ name = identifier ? identifier : cycle['id']
+ path = "/products/#{product.data['id']}/cycles/#{name}"
+ data = cycle_to_json(cycle)
+ new(site, path, data, {})
+ end
+
+ def product_to_json(site, product)
+ additional_details = {
+ links: {
+ icon: product.data['iconUrl'],
+ html: ApiV1.site_url(site, "/#{product.data['id']}"),
+ releasePolicy: product.data['releasePolicyLink'],
+ },
+ versionCommand: product.data['versionCommand'],
+ cycles: product.data['releases'].map { |cycle| cycle_to_json(cycle) }
+ }
+
+ product_summary_to_json(site, product).except(:uri).merge(additional_details)
+ end
+
+ def product_summary_to_json(site, product)
+ {
+ name: product.data['id'],
+ label: product.data['title'],
+ category: product.data['category'],
+ tags: product.data['tags'],
+ identifiers: product.data['identifiers'].map { |identifier| {
+ type: identifier.keys.first,
+ id: identifier.values.first
+ } },
+ uri: ApiV1.api_url(site, "/products/#{product.data['id']}/")
+ }
+ end
+
+ def cycle_to_json(cycle)
+ {
+ name: cycle['releaseCycle'],
+ codename: cycle['codename'],
+ label: ApiV1.strip_html(cycle['label']),
+ date: cycle['releaseDate'],
+ support: cycle['support'],
+ lts: cycle['lts'],
+ eol: cycle['eol'],
+ discontinued: cycle['discontinued'],
+ extendedSupport: cycle['extendedSupport'],
+ latest: {
+ name: cycle['latest'],
+ date: cycle['latestReleaseDate'],
+ link: cycle['link'],
+ }
+ }
+ 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'] = metadata
+ @data['data']['result'] = data
+ @data['data']['schema_version'] = ApiV1::VERSION
+
+ self.process(@name)
+ end
end
end
-
-# Write /all.json
-all_products_file = ApiV1::file('all.json')
-File.open(all_products_file, 'w') { |f| f.puts product_names.sort.to_json }
diff --git a/_plugins/product-data-enricher.rb b/_plugins/product-data-enricher.rb
index 30f8a110afae..29ee1722ae8d 100644
--- a/_plugins/product-data-enricher.rb
+++ b/_plugins/product-data-enricher.rb
@@ -13,6 +13,7 @@ def enrich(page)
set_description(page)
set_icon_url(page)
set_tags(page)
+ set_identifiers(page)
set_overridden_columns_label(page)
page.data["releases"].each { |release| enrich_release(page, release) }
@@ -61,6 +62,13 @@ def set_tags(page)
page.data['tags'] = tags
end
+ # Set identifiers to empty if it's not present.
+ def set_identifiers(page)
+ if !page.data['identifiers']
+ page.data['identifiers'] = []
+ end
+ end
+
# Set properly the column presence/label if it was overridden.
def set_overridden_columns_label(page)
date_column_names = [
diff --git a/_redirects b/_redirects
index b011abd794ca..a24919e09d05 100644
--- a/_redirects
+++ b/_redirects
@@ -9,7 +9,14 @@
# Setting a layout forces Jekyll to render this file
layout: null
---
-{%- for page in site.pages -%}
+# 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.
+/api/v1/* /api/v1/:splat/index.json 200!
+
+{% for page in site.pages -%}
# Redirects for {{page.path}}
{%- if page.alternate_urls %}
{%- for url in page.alternate_urls %}
@@ -28,6 +35,3 @@ layout: null
{%- endif %}
{% endfor %}
-
-# Send API 404 responses in JSON
-/api/* /assets/404.json 404
diff --git a/api_v1/openapi.yml b/api_v1/openapi.yml
new file mode 100644
index 000000000000..994290268afe
--- /dev/null
+++ b/api_v1/openapi.yml
@@ -0,0 +1,454 @@
+---
+# 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.0.3
+
+info:
+ title: endoflife API
+ version: "1.0.0-b1"
+ license:
+ name: MIT License
+ url: 'https://github.com/endoflife-date/endoflife.date/blob/master/LICENSE'
+ description: >-
+ endoflife.date documents EOL dates and support lifecycles for various products.
+ The endoflife API allows users to discover and query for those products.
+
+
+ Some useful links:
+
+ - [The endoflife.date website](https://endoflife.date/)
+
+ - [The endoflife.date repository](https://github.com/endoflife-date/endoflife.date)
+
+ - [The endoflife.date issue tracker](https://github.com/endoflife-date/endoflife.date/issues/)
+
+ - [The source API definition](https://github.com/endoflife-date/endoflife.date/blob/master/assets/openapi.yml)
+
+# Replace with your preview URL (such as https://deploy-preview-2080--endoflife-date.netlify.app/api/v1).
+servers:
+ - url: {{ site.url }}/api/v1
+
+paths:
+ /:
+ get:
+ summary: List the main endoflife.date API endpoints.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UriListResponse'
+
+ /products:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date.
+ Only a subset of each product's data is returned by this endpoint.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductListResponse'
+
+ /products/{product}/:
+ get:
+ summary: >
+ Get the given product data.
+ This endpoint is returning all endoflife.date knows about the product, including release
+ cycles data.
+ parameters:
+ - name: product
+ in: path
+ description: 'The name of the product.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductResponse'
+
+ /products/{product}/cycles/{cycle}:
+ get:
+ summary: Get the given product release cycle data.
+ parameters:
+ - name: product
+ in: path
+ description: 'The name of the product.'
+ required: true
+ schema:
+ type: string
+ - name: cycle
+ in: path
+ description: 'The name of the cycle.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductCycleResponse'
+
+ /products/{product}/cycles/latest/:
+ get:
+ summary: Get the latest release cycle data for the given product.
+ parameters:
+ - name: product
+ in: path
+ description: 'The name of the product.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductCycleResponse'
+
+ /categories:
+ get:
+ summary: List all endoflife.date categories.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UriListResponse'
+
+ /categories/{category}:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date for the given category.
+ Only a subset of each product's data is returned by this endpoint.
+ parameters:
+ - name: category
+ in: path
+ description: 'The name of the category.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductListResponse'
+
+ /tags:
+ get:
+ summary: List all endoflife.date tags.
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/UriListResponse'
+
+ /tags/{tag}:
+ get:
+ summary: >
+ List all the products referenced on endoflife.date for the given tag.
+ Only a subset of each product's data is returned by this endpoint.
+ parameters:
+ - name: tag
+ in: path
+ description: 'The name of the tag.'
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ description: successful operation
+ headers: {}
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/ProductListResponse'
+
+# Responses must be at the end of the list, contain a schema_version property and be suffixed with
+# 'Response' to facilitate maintenance and reading.
+components:
+ schemas:
+ Uri:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the URI
+ example: tags
+ uri:
+ type: string
+ format: uri
+ description: URI
+ example: {{ site.url }}/tags/
+
+ Identifier:
+ type: object
+ properties:
+ name:
+ type: type
+ description: Type of the identifier (types as of 2023-03 are repology, purl and cpe)
+ example: purl
+ id:
+ type: string
+ description: Identifier
+ example: cpe:/o:almalinux:almalinux
+
+ ProductVersion:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the version.
+ example: "11.6"
+ date:
+ type: string
+ format: date
+ description: Release date.
+ example: "2022-12-17"
+ link:
+ type: string
+ format: uri
+ description: Link to the changelog or release notes.
+ example: https://www.debian.org/News/2022/2022091002
+
+ ProductCycle:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the product cycle.
+ example: "11"
+ codename:
+ type: string
+ nullable: true
+ description: Name of the product cycle.
+ example: Bullseye
+ label:
+ type: string
+ description: Label of the product cycle.
+ example: 11 (Bullseye) (Upcoming LTS)
+ date:
+ type: string
+ format: date
+ description: Date of the first version of the cycle.
+ example: "2021-08-14"
+ support:
+ oneOf:
+ - type: string
+ format: date
+ - type: boolean
+ nullable: true
+ description: End of active support date, or true / false to indicate whether or not the product cycle is still supported.
+ example: "2021-08-14"
+ lts:
+ oneOf:
+ - type: string
+ format: date
+ - type: boolean
+ nullable: true
+ description: Start date of the LTS phase, or true / false to indicate whether or not the product cycle is LTS.
+ example: "2021-08-14"
+ eol:
+ oneOf:
+ - type: string
+ format: date
+ - type: boolean
+ description: End of life date, or true / false to indicate whether or not the product cycle is EOL.
+ example: "2026-08-15"
+ discontinued:
+ oneOf:
+ - type: string
+ format: date
+ - type: boolean
+ nullable: true
+ description: Discontinued date, or true / false to indicate whether or not the product cycle is discontinued (mainly used for hardware).
+ example: "2026-08-15"
+ extendedSupport:
+ oneOf:
+ - type: string
+ format: date
+ - type: boolean
+ nullable: true
+ description: End of extended support date, or true / false to indicate whether or not the product cycle is still in the extended support phase.
+ example: "2029-08-15"
+ latest:
+ type: object
+ $ref: '#/components/schemas/ProductVersion'
+
+ ProductSummary:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the product
+ example: debian
+ label:
+ type: string
+ description: Label of the product
+ example: Debian GNU/Linux
+ category:
+ type: string
+ description: Category of the product
+ example: os
+ tags:
+ type: array
+ description: Tags associated to the product
+ items:
+ type: string
+ identifiers:
+ type: array
+ description: Known identifiers (purl, repology, cpe...) associated to the product
+ items:
+ $ref: '#/components/schemas/Identifier'
+ uri:
+ type: string
+ format: uri
+ description: Link to the full product details
+ example: {{ site.url }}/api/v1/products/debian/
+
+ ProductDetails:
+ type: object
+ properties:
+ name:
+ type: string
+ description: Name of the product
+ example: debian
+ label:
+ type: string
+ description: Label of the product
+ example: Debian GNU/Linux
+ category:
+ type: string
+ description: Category of the product
+ example: os
+ tags:
+ type: array
+ description: Tags associated to the product
+ items:
+ type: string
+ identifiers:
+ type: array
+ description: Known identifiers (purl, repology, cpe...) associated to the product
+ items:
+ $ref: '#/components/schemas/Identifier'
+ # Additional properties (compared to ProductSummary)
+ links:
+ type: object
+ description: Product links.
+ properties:
+ icon:
+ type: string
+ format: uri
+ nullable: true
+ description: Link to the product icon (on https://simpleicons.org/).
+ example: https://simpleicons.org/icons/debian.svg
+ html:
+ type: string
+ format: uri
+ description: Link to the product page on endoflife.date.
+ example: https://endoflife.date/debian
+ releasePolicy:
+ type: string
+ format: uri
+ nullable: true
+ description: Link to the product release policy.
+ example: https://wiki.debian.org/DebianReleases
+ versionCommand:
+ type: string
+ description: Command that can be used to check the current product version.
+ example: cat /etc/os-release
+ cycles:
+ type: array
+ description: Product release cycles.
+ items:
+ $ref: '#/components/schemas/ProductCycle'
+
+ UriListResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ total:
+ type: integer
+ format: int32
+ description: Number of uri in the list.
+ example: 1
+ result:
+ type: array
+ items:
+ $ref: '#/components/schemas/Uri'
+
+ ProductListResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ total:
+ type: integer
+ format: int32
+ description: Number of products in the list.
+ example: 1
+ result:
+ type: array
+ items:
+ $ref: '#/components/schemas/ProductSummary'
+
+ ProductCycleResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ result:
+ $ref: '#/components/schemas/ProductCycle'
+
+ ProductResponse:
+ type: object
+ properties:
+ schema_version:
+ type: string
+ description: Version of this schema.
+ example: 1.0.0
+ last_modified:
+ type: string
+ format: date-time
+ description: The time this product was last modified.
+ example: 2023-03-01T14:05:52+01:00
+ auto:
+ type: boolean
+ description: Whether or not product versions are automatically updated.
+ example: true
+ result:
+ $ref: '#/components/schemas/ProductDetails'
diff --git a/api_v1/swagger-ui.md b/api_v1/swagger-ui.md
new file mode 100644
index 000000000000..bd2a634e7b01
--- /dev/null
+++ b/api_v1/swagger-ui.md
@@ -0,0 +1,6 @@
+---
+title: EndOfLife API v1 Swagger UI
+permalink: /docs/api/v1/
+openapi_yml: /docs/api/v1/openapi.yml
+layout: swagger-ui
+---
diff --git a/assets/404.json b/assets/404.json
deleted file mode 100644
index 0238a1f9945e..000000000000
--- a/assets/404.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "message": "Product not found"
-}
\ No newline at end of file
diff --git a/humans.txt b/humans.txt
index 33ff45aba201..4843c09bdbd4 100644
--- a/humans.txt
+++ b/humans.txt
@@ -5,5 +5,5 @@ Contributors: https://github.com/endoflife-date/endoflife.date/graphs/contributo
/* SITE */
Software: Jekyll, Netlify, GitHub, Ruby, GitHub Actions
-Components: Just the Docs Jekyll Theme, Stoplight Elements, Simple Icons
+Components: Just the Docs Jekyll Theme, Stoplight Elements, Swagger UI, Simple Icons
Logo: adaptation of "An hourglass in a round icon" by David Abián and Serhio Magpie (https://commons.wikimedia.org/wiki/File:Hourglass_icon_%28orange%29.svg)
diff --git a/index.md b/index.md
index 167c6ea1c338..8309e6e0910f 100644
--- a/index.md
+++ b/index.md
@@ -10,7 +10,7 @@ End-of-life (EOL) and support information is [often hard to track, or very badly
endoflife.date documents EOL dates and support lifecycles for various products.
endoflife.date aggregates data from various sources and presents it in an understandable and
-succinct manner. It also makes the data available using an [easily accessible API](https://endoflife.date/docs/api)
+succinct manner. It also makes the data available using an [easily accessible API](/docs/api/v1/)
and has iCalendar support.
endoflife.date currently tracks {{ site.pages | where: "layout", "product" | size }} products.
@@ -40,7 +40,7 @@ If you maintain release information for a product (end-of-life dates or support
also have a [set of recommendations](/recommendations) along with a checklist on some best practices
for publishing this information.
-And do not hesitate to [play with our API](https://endoflife.date/docs/api). Here are a few awesome
+And do not hesitate to [play with our API](/docs/api/v1/). Here are a few awesome
tools that already did it: [norwegianblue](https://github.com/hugovk/norwegianblue),
[end_of_life](https://github.com/MatheusRich/end_of_life), and
[cicada](https://github.com/mcandre/cicada). Find more on