├── .nvmrc
├── .env.dev
├── .gitignore
├── requirements.txt
├── static
├── og-image.webp
├── rss-anything-example.webp
├── custom.css
├── diffbot-logomark.svg
└── main.css
├── tailwind.config.js
├── docker-compose.yml
├── Dockerfile
├── README.md
├── LICENSE
├── app.py
└── templates
└── home.html
/.nvmrc:
--------------------------------------------------------------------------------
1 | v20.3.0
--------------------------------------------------------------------------------
/.env.dev:
--------------------------------------------------------------------------------
1 | DIFFBOT_TOKEN=
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | __pycache__
3 | .env
4 | env
5 | *.sh
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | feedgen
2 | Flask
3 | Flask-Limiter
4 | gunicorn
5 | python-dotenv
6 | requests
--------------------------------------------------------------------------------
/static/og-image.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diffbot/rss-anything/HEAD/static/og-image.webp
--------------------------------------------------------------------------------
/static/rss-anything-example.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/diffbot/rss-anything/HEAD/static/rss-anything-example.webp
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: ["./templates/**/*.{html,js}"],
4 | theme: {
5 | extend: {},
6 | },
7 | plugins: [],
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.7'
2 |
3 | services:
4 | rssanything:
5 | restart: always
6 | container_name: rssanything
7 | build:
8 | context: .
9 | dockerfile: Dockerfile
10 | ports:
11 | - '3012:8000'
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # base image
2 | FROM nikolaik/python-nodejs:latest
3 |
4 | WORKDIR /app
5 | COPY ./ /app
6 |
7 | # install requirements
8 | RUN pip install -r requirements.txt
9 |
10 | # start app
11 | EXPOSE 8000
12 | CMD ["gunicorn", "app:app", "-b", "0.0.0.0:8000", "-w", "4", "--threads", "10"]
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # RSS Anything
2 |
3 | Uses [Diffbot's Extract API](https://www.diffbot.com/products/extract/) to transform lists of links on websites into an RSS feed.
4 |
5 | ## Build Locally
6 |
7 | **Requirements**
8 | * Python 3+
9 | * [virtualenv](https://virtualenv.pypa.io/en/latest/) recommended but not necessary.
10 | * npm
11 |
12 | ```sh
13 | pip install requirements.txt
14 | npm install tailwindcss
15 | npx tailwindcss -i ./static/custom.css -o ./static/main.css
16 | flask run
17 | ```
--------------------------------------------------------------------------------
/static/custom.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | -webkit-font-smoothing: antialiased;
7 | background: rgb(255,251,245);
8 | background: linear-gradient(180deg, rgba(255,251,245,1) 0%, rgba(255,255,255,1) 100%);
9 | }
10 |
11 | h1.title-font {
12 | font-family:"Prata";
13 | }
14 |
15 | .github-triangle {
16 | width: 0;
17 | height: 0;
18 | border-style: solid;
19 | border-top: 90px solid rgb(245,241,235);;
20 | border-left: 90px solid transparent;
21 | right: 0;
22 | top: 0;
23 | position: absolute;
24 | }
25 |
26 | .github-triangle a {
27 | position: absolute;
28 | top: -75px;
29 | right: 15px;
30 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Diffbot
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import requests
2 | import os
3 | import secrets
4 | from datetime import datetime
5 | from dotenv import load_dotenv
6 | from feedgen.feed import FeedGenerator
7 | from flask import Flask, request, make_response, render_template, flash
8 | from flask_limiter import Limiter
9 | from flask_limiter.util import get_remote_address
10 | from urllib.parse import quote_plus, unquote
11 |
12 | app = Flask(__name__)
13 | app.secret_key = secrets.token_hex() # No need to persist this key between resets
14 |
15 | load_dotenv()
16 | DIFFBOT_TOKEN = os.getenv("DIFFBOT_TOKEN", None)
17 |
18 | # Really basic rate limiting to avoid taking down the app by bad actors
19 | limiter = Limiter(
20 | get_remote_address,
21 | app=app,
22 | storage_uri='memory://'
23 | )
24 |
25 | @app.route('/')
26 | def index():
27 | return render_template('home.html')
28 |
29 | @app.route('/feeds')
30 | def feeds():
31 | feed_sample = []
32 | feed_detail = {}
33 | actual_rss_url = ""
34 | try:
35 | # Attempt to Generate an RSS Feed
36 | fg, actual_rss_url = generate_feed(request.args.get('url', None))
37 | # Feed Details
38 | feed_detail = {
39 | "title": fg.title(),
40 | "description": fg.description(),
41 | "link": fg.link(),
42 | "icon": fg.icon()
43 | }
44 | except Exception as e:
45 | flash(str(e), 'Error')
46 | return render_template('home.html', page_url=request.args.get('url', ''), feed_detail=feed_detail, actual_rss_url=actual_rss_url)
47 |
48 | @app.route('/rss')
49 | @limiter.limit("1/second", error_message='Rate limit exceeded')
50 | def rss():
51 | try:
52 | fg, rss_url = generate_feed(request.args.get('url', None))
53 | response = make_response(fg.rss_str())
54 | response.headers.set('Content-Type', 'application/rss+xml')
55 | return response
56 | except Exception as e:
57 | return make_response(str(e), 400)
58 |
59 | @app.route('/atom')
60 | @limiter.limit("1/second", error_message='Rate limit exceeded')
61 | def atom():
62 | try:
63 | fg, rss_url = generate_feed(request.args.get('url', None))
64 | response = make_response(fg.atom_str())
65 | response.headers.set('Content-Type', 'application/atom+xml')
66 | return response
67 | except Exception as e:
68 | return make_response(str(e), 400)
69 |
70 | def generate_feed(url):
71 | # 1. Extract list from URL
72 | list_url = url
73 | feed_items = []
74 | feed_title = ""
75 | feed_description = ""
76 | feed_icon= ""
77 | feed_url = ""
78 | actual_rss_url = None
79 |
80 | if not list_url:
81 | raise Exception("No URL Provided")
82 |
83 | try:
84 | payload = {
85 | 'token': DIFFBOT_TOKEN,
86 | 'url': unquote(list_url),
87 | 'paging': "false"
88 | }
89 | extracted_list_response = requests.get(f"https://api.diffbot.com/v3/list", params=payload)
90 | extracted_list = extracted_list_response.json()
91 | if extracted_list.get("error", None):
92 | raise Exception(extracted_list.get("error", "Page Error"))
93 | feed_items = extracted_list.get("objects", [])[0].get("items", [])
94 | feed_title = extracted_list.get("objects", [])[0].get("title", feed_url)
95 | feed_description = extracted_list.get("objects", [])[0].get("pageUrl", "")
96 | feed_icon = extracted_list.get("objects", [])[0].get("icon", "")
97 | feed_url = extracted_list.get("objects", [])[0].get("pageUrl", "")
98 | actual_rss_url = extracted_list.get("objects", [])[0].get("rss_url", None)
99 | except Exception as e:
100 | print("Exception: ", e)
101 | raise Exception(str(e))
102 |
103 | # 2. Instantiate a Feed
104 | fg = FeedGenerator()
105 | fg.load_extension('media')
106 | fg.title(feed_title if feed_title else feed_url)
107 | fg.id(feed_url)
108 | fg.description(feed_description)
109 | fg.icon(feed_icon)
110 | fg.link(href=f"https://rss.diffbot.com/rss?url={feed_url}", rel='self')
111 | fg.link(href=feed_url, rel='alternate')
112 | fg.managingEditor(managingEditor="jerome@diffbot.com (Jerome Choo)")
113 | fg.docs(docs="https://rss.diffbot.com")
114 |
115 | # 3. Generate feed item from list items
116 | for article in reversed(feed_items):
117 | if article.get("title", None) and article.get("link", None):
118 | fe = fg.add_entry()
119 | fe.title(article.get("title", ""))
120 | fe.id(article.get("link", ""))
121 | fe.link(href=article.get("link", ""))
122 | if image := article.get("image", None):
123 | fe.enclosure(url=image, length=0, type='image/jpeg')
124 | fe.media.thumbnail(url=image)
125 | fe.description(article.get("summary", ""))
126 | if author := article.get("byline", None) or article.get("author", None):
127 | fe.author(name=author)
128 | if published_date := article.get("date", None):
129 | fe.pubDate(published_date)
130 | return fg, actual_rss_url
--------------------------------------------------------------------------------
/static/diffbot-logomark.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Diffbot-Design-System_diffy-light-background
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/templates/home.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | RSS Anything
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | {% if feed_detail %}
26 |
27 |
28 | {% endif %}
29 | {% if actual_rss_url %}
30 |
31 | {% endif %}
32 |
33 |
34 |
35 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | RSS Anything
47 |
48 |
Transform any old website with a list of links into an RSS Feed
49 |
50 |
55 |
56 | {% if feed_detail %}
57 |
58 |
59 |
60 |
61 |
{{ feed_detail['title'] }}
62 |
63 | {% if actual_rss_url %}
64 | 📣 Heads up! This website exposes its own RSS feed. Give the original RSS feed a shot, then try our generated feeds if it's not what you're looking for.
65 | {% else %}
66 | 🌟 Easy peazy! Just add one of the feed URLs below (use RSS if you're unsure) to your reader of choice and you're off to the races.
67 | {% endif %}
68 |
69 |
91 |
92 |
93 |
94 |
95 | {% endif %}
96 | {% with messages = get_flashed_messages(with_categories=true) %}
97 | {% if messages %}
98 |
99 |
100 | {% for category, message in messages %}
101 |
102 | {{ category }} — {{ message }}
103 |
104 | {% endfor %}
105 |
106 |
107 | {% endif %}
108 | {% endwith %}
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 | With ❤️ from Diffbot
125 | Because Jerome wants to read all the things in RSS
126 |
127 |
128 |
129 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/static/main.css:
--------------------------------------------------------------------------------
1 | /*
2 | ! tailwindcss v3.3.5 | MIT License | https://tailwindcss.com
3 | */
4 |
5 | /*
6 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
7 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
8 | */
9 |
10 | *,
11 | ::before,
12 | ::after {
13 | box-sizing: border-box;
14 | /* 1 */
15 | border-width: 0;
16 | /* 2 */
17 | border-style: solid;
18 | /* 2 */
19 | border-color: #e5e7eb;
20 | /* 2 */
21 | }
22 |
23 | ::before,
24 | ::after {
25 | --tw-content: '';
26 | }
27 |
28 | /*
29 | 1. Use a consistent sensible line-height in all browsers.
30 | 2. Prevent adjustments of font size after orientation changes in iOS.
31 | 3. Use a more readable tab size.
32 | 4. Use the user's configured `sans` font-family by default.
33 | 5. Use the user's configured `sans` font-feature-settings by default.
34 | 6. Use the user's configured `sans` font-variation-settings by default.
35 | */
36 |
37 | html {
38 | line-height: 1.5;
39 | /* 1 */
40 | -webkit-text-size-adjust: 100%;
41 | /* 2 */
42 | -moz-tab-size: 4;
43 | /* 3 */
44 | -o-tab-size: 4;
45 | tab-size: 4;
46 | /* 3 */
47 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
48 | /* 4 */
49 | font-feature-settings: normal;
50 | /* 5 */
51 | font-variation-settings: normal;
52 | /* 6 */
53 | }
54 |
55 | /*
56 | 1. Remove the margin in all browsers.
57 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
58 | */
59 |
60 | body {
61 | margin: 0;
62 | /* 1 */
63 | line-height: inherit;
64 | /* 2 */
65 | }
66 |
67 | /*
68 | 1. Add the correct height in Firefox.
69 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
70 | 3. Ensure horizontal rules are visible by default.
71 | */
72 |
73 | hr {
74 | height: 0;
75 | /* 1 */
76 | color: inherit;
77 | /* 2 */
78 | border-top-width: 1px;
79 | /* 3 */
80 | }
81 |
82 | /*
83 | Add the correct text decoration in Chrome, Edge, and Safari.
84 | */
85 |
86 | abbr:where([title]) {
87 | -webkit-text-decoration: underline dotted;
88 | text-decoration: underline dotted;
89 | }
90 |
91 | /*
92 | Remove the default font size and weight for headings.
93 | */
94 |
95 | h1,
96 | h2,
97 | h3,
98 | h4,
99 | h5,
100 | h6 {
101 | font-size: inherit;
102 | font-weight: inherit;
103 | }
104 |
105 | /*
106 | Reset links to optimize for opt-in styling instead of opt-out.
107 | */
108 |
109 | a {
110 | color: inherit;
111 | text-decoration: inherit;
112 | }
113 |
114 | /*
115 | Add the correct font weight in Edge and Safari.
116 | */
117 |
118 | b,
119 | strong {
120 | font-weight: bolder;
121 | }
122 |
123 | /*
124 | 1. Use the user's configured `mono` font family by default.
125 | 2. Correct the odd `em` font sizing in all browsers.
126 | */
127 |
128 | code,
129 | kbd,
130 | samp,
131 | pre {
132 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
133 | /* 1 */
134 | font-size: 1em;
135 | /* 2 */
136 | }
137 |
138 | /*
139 | Add the correct font size in all browsers.
140 | */
141 |
142 | small {
143 | font-size: 80%;
144 | }
145 |
146 | /*
147 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
148 | */
149 |
150 | sub,
151 | sup {
152 | font-size: 75%;
153 | line-height: 0;
154 | position: relative;
155 | vertical-align: baseline;
156 | }
157 |
158 | sub {
159 | bottom: -0.25em;
160 | }
161 |
162 | sup {
163 | top: -0.5em;
164 | }
165 |
166 | /*
167 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
168 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
169 | 3. Remove gaps between table borders by default.
170 | */
171 |
172 | table {
173 | text-indent: 0;
174 | /* 1 */
175 | border-color: inherit;
176 | /* 2 */
177 | border-collapse: collapse;
178 | /* 3 */
179 | }
180 |
181 | /*
182 | 1. Change the font styles in all browsers.
183 | 2. Remove the margin in Firefox and Safari.
184 | 3. Remove default padding in all browsers.
185 | */
186 |
187 | button,
188 | input,
189 | optgroup,
190 | select,
191 | textarea {
192 | font-family: inherit;
193 | /* 1 */
194 | font-feature-settings: inherit;
195 | /* 1 */
196 | font-variation-settings: inherit;
197 | /* 1 */
198 | font-size: 100%;
199 | /* 1 */
200 | font-weight: inherit;
201 | /* 1 */
202 | line-height: inherit;
203 | /* 1 */
204 | color: inherit;
205 | /* 1 */
206 | margin: 0;
207 | /* 2 */
208 | padding: 0;
209 | /* 3 */
210 | }
211 |
212 | /*
213 | Remove the inheritance of text transform in Edge and Firefox.
214 | */
215 |
216 | button,
217 | select {
218 | text-transform: none;
219 | }
220 |
221 | /*
222 | 1. Correct the inability to style clickable types in iOS and Safari.
223 | 2. Remove default button styles.
224 | */
225 |
226 | button,
227 | [type='button'],
228 | [type='reset'],
229 | [type='submit'] {
230 | -webkit-appearance: button;
231 | /* 1 */
232 | background-color: transparent;
233 | /* 2 */
234 | background-image: none;
235 | /* 2 */
236 | }
237 |
238 | /*
239 | Use the modern Firefox focus style for all focusable elements.
240 | */
241 |
242 | :-moz-focusring {
243 | outline: auto;
244 | }
245 |
246 | /*
247 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
248 | */
249 |
250 | :-moz-ui-invalid {
251 | box-shadow: none;
252 | }
253 |
254 | /*
255 | Add the correct vertical alignment in Chrome and Firefox.
256 | */
257 |
258 | progress {
259 | vertical-align: baseline;
260 | }
261 |
262 | /*
263 | Correct the cursor style of increment and decrement buttons in Safari.
264 | */
265 |
266 | ::-webkit-inner-spin-button,
267 | ::-webkit-outer-spin-button {
268 | height: auto;
269 | }
270 |
271 | /*
272 | 1. Correct the odd appearance in Chrome and Safari.
273 | 2. Correct the outline style in Safari.
274 | */
275 |
276 | [type='search'] {
277 | -webkit-appearance: textfield;
278 | /* 1 */
279 | outline-offset: -2px;
280 | /* 2 */
281 | }
282 |
283 | /*
284 | Remove the inner padding in Chrome and Safari on macOS.
285 | */
286 |
287 | ::-webkit-search-decoration {
288 | -webkit-appearance: none;
289 | }
290 |
291 | /*
292 | 1. Correct the inability to style clickable types in iOS and Safari.
293 | 2. Change font properties to `inherit` in Safari.
294 | */
295 |
296 | ::-webkit-file-upload-button {
297 | -webkit-appearance: button;
298 | /* 1 */
299 | font: inherit;
300 | /* 2 */
301 | }
302 |
303 | /*
304 | Add the correct display in Chrome and Safari.
305 | */
306 |
307 | summary {
308 | display: list-item;
309 | }
310 |
311 | /*
312 | Removes the default spacing and border for appropriate elements.
313 | */
314 |
315 | blockquote,
316 | dl,
317 | dd,
318 | h1,
319 | h2,
320 | h3,
321 | h4,
322 | h5,
323 | h6,
324 | hr,
325 | figure,
326 | p,
327 | pre {
328 | margin: 0;
329 | }
330 |
331 | fieldset {
332 | margin: 0;
333 | padding: 0;
334 | }
335 |
336 | legend {
337 | padding: 0;
338 | }
339 |
340 | ol,
341 | ul,
342 | menu {
343 | list-style: none;
344 | margin: 0;
345 | padding: 0;
346 | }
347 |
348 | /*
349 | Reset default styling for dialogs.
350 | */
351 |
352 | dialog {
353 | padding: 0;
354 | }
355 |
356 | /*
357 | Prevent resizing textareas horizontally by default.
358 | */
359 |
360 | textarea {
361 | resize: vertical;
362 | }
363 |
364 | /*
365 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
366 | 2. Set the default placeholder color to the user's configured gray 400 color.
367 | */
368 |
369 | input::-moz-placeholder, textarea::-moz-placeholder {
370 | opacity: 1;
371 | /* 1 */
372 | color: #9ca3af;
373 | /* 2 */
374 | }
375 |
376 | input::placeholder,
377 | textarea::placeholder {
378 | opacity: 1;
379 | /* 1 */
380 | color: #9ca3af;
381 | /* 2 */
382 | }
383 |
384 | /*
385 | Set the default cursor for buttons.
386 | */
387 |
388 | button,
389 | [role="button"] {
390 | cursor: pointer;
391 | }
392 |
393 | /*
394 | Make sure disabled buttons don't get the pointer cursor.
395 | */
396 |
397 | :disabled {
398 | cursor: default;
399 | }
400 |
401 | /*
402 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
403 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
404 | This can trigger a poorly considered lint error in some tools but is included by design.
405 | */
406 |
407 | img,
408 | svg,
409 | video,
410 | canvas,
411 | audio,
412 | iframe,
413 | embed,
414 | object {
415 | display: block;
416 | /* 1 */
417 | vertical-align: middle;
418 | /* 2 */
419 | }
420 |
421 | /*
422 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
423 | */
424 |
425 | img,
426 | video {
427 | max-width: 100%;
428 | height: auto;
429 | }
430 |
431 | /* Make elements with the HTML hidden attribute stay hidden by default */
432 |
433 | [hidden] {
434 | display: none;
435 | }
436 |
437 | *, ::before, ::after {
438 | --tw-border-spacing-x: 0;
439 | --tw-border-spacing-y: 0;
440 | --tw-translate-x: 0;
441 | --tw-translate-y: 0;
442 | --tw-rotate: 0;
443 | --tw-skew-x: 0;
444 | --tw-skew-y: 0;
445 | --tw-scale-x: 1;
446 | --tw-scale-y: 1;
447 | --tw-pan-x: ;
448 | --tw-pan-y: ;
449 | --tw-pinch-zoom: ;
450 | --tw-scroll-snap-strictness: proximity;
451 | --tw-gradient-from-position: ;
452 | --tw-gradient-via-position: ;
453 | --tw-gradient-to-position: ;
454 | --tw-ordinal: ;
455 | --tw-slashed-zero: ;
456 | --tw-numeric-figure: ;
457 | --tw-numeric-spacing: ;
458 | --tw-numeric-fraction: ;
459 | --tw-ring-inset: ;
460 | --tw-ring-offset-width: 0px;
461 | --tw-ring-offset-color: #fff;
462 | --tw-ring-color: rgb(59 130 246 / 0.5);
463 | --tw-ring-offset-shadow: 0 0 #0000;
464 | --tw-ring-shadow: 0 0 #0000;
465 | --tw-shadow: 0 0 #0000;
466 | --tw-shadow-colored: 0 0 #0000;
467 | --tw-blur: ;
468 | --tw-brightness: ;
469 | --tw-contrast: ;
470 | --tw-grayscale: ;
471 | --tw-hue-rotate: ;
472 | --tw-invert: ;
473 | --tw-saturate: ;
474 | --tw-sepia: ;
475 | --tw-drop-shadow: ;
476 | --tw-backdrop-blur: ;
477 | --tw-backdrop-brightness: ;
478 | --tw-backdrop-contrast: ;
479 | --tw-backdrop-grayscale: ;
480 | --tw-backdrop-hue-rotate: ;
481 | --tw-backdrop-invert: ;
482 | --tw-backdrop-opacity: ;
483 | --tw-backdrop-saturate: ;
484 | --tw-backdrop-sepia: ;
485 | }
486 |
487 | ::backdrop {
488 | --tw-border-spacing-x: 0;
489 | --tw-border-spacing-y: 0;
490 | --tw-translate-x: 0;
491 | --tw-translate-y: 0;
492 | --tw-rotate: 0;
493 | --tw-skew-x: 0;
494 | --tw-skew-y: 0;
495 | --tw-scale-x: 1;
496 | --tw-scale-y: 1;
497 | --tw-pan-x: ;
498 | --tw-pan-y: ;
499 | --tw-pinch-zoom: ;
500 | --tw-scroll-snap-strictness: proximity;
501 | --tw-gradient-from-position: ;
502 | --tw-gradient-via-position: ;
503 | --tw-gradient-to-position: ;
504 | --tw-ordinal: ;
505 | --tw-slashed-zero: ;
506 | --tw-numeric-figure: ;
507 | --tw-numeric-spacing: ;
508 | --tw-numeric-fraction: ;
509 | --tw-ring-inset: ;
510 | --tw-ring-offset-width: 0px;
511 | --tw-ring-offset-color: #fff;
512 | --tw-ring-color: rgb(59 130 246 / 0.5);
513 | --tw-ring-offset-shadow: 0 0 #0000;
514 | --tw-ring-shadow: 0 0 #0000;
515 | --tw-shadow: 0 0 #0000;
516 | --tw-shadow-colored: 0 0 #0000;
517 | --tw-blur: ;
518 | --tw-brightness: ;
519 | --tw-contrast: ;
520 | --tw-grayscale: ;
521 | --tw-hue-rotate: ;
522 | --tw-invert: ;
523 | --tw-saturate: ;
524 | --tw-sepia: ;
525 | --tw-drop-shadow: ;
526 | --tw-backdrop-blur: ;
527 | --tw-backdrop-brightness: ;
528 | --tw-backdrop-contrast: ;
529 | --tw-backdrop-grayscale: ;
530 | --tw-backdrop-hue-rotate: ;
531 | --tw-backdrop-invert: ;
532 | --tw-backdrop-opacity: ;
533 | --tw-backdrop-saturate: ;
534 | --tw-backdrop-sepia: ;
535 | }
536 |
537 | .container {
538 | width: 100%;
539 | }
540 |
541 | @media (min-width: 640px) {
542 | .container {
543 | max-width: 640px;
544 | }
545 | }
546 |
547 | @media (min-width: 768px) {
548 | .container {
549 | max-width: 768px;
550 | }
551 | }
552 |
553 | @media (min-width: 1024px) {
554 | .container {
555 | max-width: 1024px;
556 | }
557 | }
558 |
559 | @media (min-width: 1280px) {
560 | .container {
561 | max-width: 1280px;
562 | }
563 | }
564 |
565 | @media (min-width: 1536px) {
566 | .container {
567 | max-width: 1536px;
568 | }
569 | }
570 |
571 | .sr-only {
572 | position: absolute;
573 | width: 1px;
574 | height: 1px;
575 | padding: 0;
576 | margin: -1px;
577 | overflow: hidden;
578 | clip: rect(0, 0, 0, 0);
579 | white-space: nowrap;
580 | border-width: 0;
581 | }
582 |
583 | .static {
584 | position: static;
585 | }
586 |
587 | .mx-auto {
588 | margin-left: auto;
589 | margin-right: auto;
590 | }
591 |
592 | .mb-1 {
593 | margin-bottom: 0.25rem;
594 | }
595 |
596 | .mb-10 {
597 | margin-bottom: 2.5rem;
598 | }
599 |
600 | .mb-2 {
601 | margin-bottom: 0.5rem;
602 | }
603 |
604 | .mb-3 {
605 | margin-bottom: 0.75rem;
606 | }
607 |
608 | .mb-4 {
609 | margin-bottom: 1rem;
610 | }
611 |
612 | .mb-6 {
613 | margin-bottom: 1.5rem;
614 | }
615 |
616 | .me-3 {
617 | margin-inline-end: 0.75rem;
618 | }
619 |
620 | .ml-1 {
621 | margin-left: 0.25rem;
622 | }
623 |
624 | .mt-10 {
625 | margin-top: 2.5rem;
626 | }
627 |
628 | .mt-3 {
629 | margin-top: 0.75rem;
630 | }
631 |
632 | .mt-6 {
633 | margin-top: 1.5rem;
634 | }
635 |
636 | .flex {
637 | display: flex;
638 | }
639 |
640 | .inline-flex {
641 | display: inline-flex;
642 | }
643 |
644 | .h-4 {
645 | height: 1rem;
646 | }
647 |
648 | .h-full {
649 | height: 100%;
650 | }
651 |
652 | .min-h-screen {
653 | min-height: 100vh;
654 | }
655 |
656 | .w-4 {
657 | width: 1rem;
658 | }
659 |
660 | .w-auto {
661 | width: auto;
662 | }
663 |
664 | .w-full {
665 | width: 100%;
666 | }
667 |
668 | .min-w-0 {
669 | min-width: 0px;
670 | }
671 |
672 | .flex-auto {
673 | flex: 1 1 auto;
674 | }
675 |
676 | .flex-none {
677 | flex: none;
678 | }
679 |
680 | .flex-col {
681 | flex-direction: column;
682 | }
683 |
684 | .flex-wrap {
685 | flex-wrap: wrap;
686 | }
687 |
688 | .items-center {
689 | align-items: center;
690 | }
691 |
692 | .justify-center {
693 | justify-content: center;
694 | }
695 |
696 | .justify-between {
697 | justify-content: space-between;
698 | }
699 |
700 | .justify-items-start {
701 | justify-items: start;
702 | }
703 |
704 | .gap-x-2 {
705 | -moz-column-gap: 0.5rem;
706 | column-gap: 0.5rem;
707 | }
708 |
709 | .gap-x-3 {
710 | -moz-column-gap: 0.75rem;
711 | column-gap: 0.75rem;
712 | }
713 |
714 | .overflow-hidden {
715 | overflow: hidden;
716 | }
717 |
718 | .rounded {
719 | border-radius: 0.25rem;
720 | }
721 |
722 | .rounded-lg {
723 | border-radius: 0.5rem;
724 | }
725 |
726 | .rounded-md {
727 | border-radius: 0.375rem;
728 | }
729 |
730 | .border {
731 | border-width: 1px;
732 | }
733 |
734 | .border-slate-200 {
735 | --tw-border-opacity: 1;
736 | border-color: rgb(226 232 240 / var(--tw-border-opacity));
737 | }
738 |
739 | .border-opacity-50 {
740 | --tw-border-opacity: 0.5;
741 | }
742 |
743 | .bg-red-50 {
744 | --tw-bg-opacity: 1;
745 | background-color: rgb(254 242 242 / var(--tw-bg-opacity));
746 | }
747 |
748 | .bg-sky-500 {
749 | --tw-bg-opacity: 1;
750 | background-color: rgb(14 165 233 / var(--tw-bg-opacity));
751 | }
752 |
753 | .bg-white {
754 | --tw-bg-opacity: 1;
755 | background-color: rgb(255 255 255 / var(--tw-bg-opacity));
756 | }
757 |
758 | .object-cover {
759 | -o-object-fit: cover;
760 | object-fit: cover;
761 | }
762 |
763 | .object-center {
764 | -o-object-position: center;
765 | object-position: center;
766 | }
767 |
768 | .p-4 {
769 | padding: 1rem;
770 | }
771 |
772 | .p-6 {
773 | padding: 1.5rem;
774 | }
775 |
776 | .px-4 {
777 | padding-left: 1rem;
778 | padding-right: 1rem;
779 | }
780 |
781 | .px-5 {
782 | padding-left: 1.25rem;
783 | padding-right: 1.25rem;
784 | }
785 |
786 | .py-12 {
787 | padding-top: 3rem;
788 | padding-bottom: 3rem;
789 | }
790 |
791 | .py-3 {
792 | padding-top: 0.75rem;
793 | padding-bottom: 0.75rem;
794 | }
795 |
796 | .py-3\.5 {
797 | padding-top: 0.875rem;
798 | padding-bottom: 0.875rem;
799 | }
800 |
801 | .pt-1 {
802 | padding-top: 0.25rem;
803 | }
804 |
805 | .text-center {
806 | text-align: center;
807 | }
808 |
809 | .text-start {
810 | text-align: start;
811 | }
812 |
813 | .text-4xl {
814 | font-size: 2.25rem;
815 | line-height: 2.5rem;
816 | }
817 |
818 | .text-lg {
819 | font-size: 1.125rem;
820 | line-height: 1.75rem;
821 | }
822 |
823 | .text-sm {
824 | font-size: 0.875rem;
825 | line-height: 1.25rem;
826 | }
827 |
828 | .text-xl {
829 | font-size: 1.25rem;
830 | line-height: 1.75rem;
831 | }
832 |
833 | .font-bold {
834 | font-weight: 700;
835 | }
836 |
837 | .font-medium {
838 | font-weight: 500;
839 | }
840 |
841 | .font-semibold {
842 | font-weight: 600;
843 | }
844 |
845 | .text-amber-600 {
846 | --tw-text-opacity: 1;
847 | color: rgb(217 119 6 / var(--tw-text-opacity));
848 | }
849 |
850 | .text-gray-500 {
851 | --tw-text-opacity: 1;
852 | color: rgb(107 114 128 / var(--tw-text-opacity));
853 | }
854 |
855 | .text-gray-900 {
856 | --tw-text-opacity: 1;
857 | color: rgb(17 24 39 / var(--tw-text-opacity));
858 | }
859 |
860 | .text-indigo-600 {
861 | --tw-text-opacity: 1;
862 | color: rgb(79 70 229 / var(--tw-text-opacity));
863 | }
864 |
865 | .text-red-800 {
866 | --tw-text-opacity: 1;
867 | color: rgb(153 27 27 / var(--tw-text-opacity));
868 | }
869 |
870 | .text-sky-600 {
871 | --tw-text-opacity: 1;
872 | color: rgb(2 132 199 / var(--tw-text-opacity));
873 | }
874 |
875 | .text-slate-500 {
876 | --tw-text-opacity: 1;
877 | color: rgb(100 116 139 / var(--tw-text-opacity));
878 | }
879 |
880 | .text-slate-600 {
881 | --tw-text-opacity: 1;
882 | color: rgb(71 85 105 / var(--tw-text-opacity));
883 | }
884 |
885 | .text-slate-900 {
886 | --tw-text-opacity: 1;
887 | color: rgb(15 23 42 / var(--tw-text-opacity));
888 | }
889 |
890 | .text-teal-600 {
891 | --tw-text-opacity: 1;
892 | color: rgb(13 148 136 / var(--tw-text-opacity));
893 | }
894 |
895 | .text-white {
896 | --tw-text-opacity: 1;
897 | color: rgb(255 255 255 / var(--tw-text-opacity));
898 | }
899 |
900 | .antialiased {
901 | -webkit-font-smoothing: antialiased;
902 | -moz-osx-font-smoothing: grayscale;
903 | }
904 |
905 | .shadow-sm {
906 | --tw-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.05);
907 | --tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);
908 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
909 | }
910 |
911 | .ring-1 {
912 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
913 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
914 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
915 | }
916 |
917 | .ring-inset {
918 | --tw-ring-inset: inset;
919 | }
920 |
921 | .ring-white\/10 {
922 | --tw-ring-color: rgb(255 255 255 / 0.1);
923 | }
924 |
925 | body {
926 | -webkit-font-smoothing: antialiased;
927 | background: rgb(255,251,245);
928 | background: linear-gradient(180deg, rgba(255,251,245,1) 0%, rgba(255,255,255,1) 100%);
929 | }
930 |
931 | h1.title-font {
932 | font-family:"Prata";
933 | }
934 |
935 | .github-triangle {
936 | width: 0;
937 | height: 0;
938 | border-style: solid;
939 | border-top: 90px solid rgb(245,241,235);
940 | border-left: 90px solid transparent;
941 | right: 0;
942 | top: 0;
943 | position: absolute;
944 | }
945 |
946 | .github-triangle a {
947 | position: absolute;
948 | top: -75px;
949 | right: 15px;
950 | }
951 |
952 | .hover\:bg-sky-400:hover {
953 | --tw-bg-opacity: 1;
954 | background-color: rgb(56 189 248 / var(--tw-bg-opacity));
955 | }
956 |
957 | .hover\:text-gray-700:hover {
958 | --tw-text-opacity: 1;
959 | color: rgb(55 65 81 / var(--tw-text-opacity));
960 | }
961 |
962 | .hover\:underline:hover {
963 | text-decoration-line: underline;
964 | }
965 |
966 | .focus\:ring-2:focus {
967 | --tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
968 | --tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);
969 | box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow, 0 0 #0000);
970 | }
971 |
972 | .focus\:ring-inset:focus {
973 | --tw-ring-inset: inset;
974 | }
975 |
976 | .focus\:ring-sky-500:focus {
977 | --tw-ring-opacity: 1;
978 | --tw-ring-color: rgb(14 165 233 / var(--tw-ring-opacity));
979 | }
980 |
981 | .focus-visible\:outline:focus-visible {
982 | outline-style: solid;
983 | }
984 |
985 | .focus-visible\:outline-2:focus-visible {
986 | outline-width: 2px;
987 | }
988 |
989 | .focus-visible\:outline-offset-2:focus-visible {
990 | outline-offset: 2px;
991 | }
992 |
993 | .focus-visible\:outline-sky-500:focus-visible {
994 | outline-color: #0ea5e9;
995 | }
996 |
997 | @media (prefers-color-scheme: dark) {
998 | .dark\:bg-gray-800 {
999 | --tw-bg-opacity: 1;
1000 | background-color: rgb(31 41 55 / var(--tw-bg-opacity));
1001 | }
1002 |
1003 | .dark\:text-red-400 {
1004 | --tw-text-opacity: 1;
1005 | color: rgb(248 113 113 / var(--tw-text-opacity));
1006 | }
1007 | }
1008 |
1009 | @media (min-width: 640px) {
1010 | .sm\:text-5xl {
1011 | font-size: 3rem;
1012 | line-height: 1;
1013 | }
1014 |
1015 | .sm\:leading-6 {
1016 | line-height: 1.5rem;
1017 | }
1018 | }
1019 |
1020 | @media (min-width: 768px) {
1021 | .md\:gap-x-4 {
1022 | -moz-column-gap: 1rem;
1023 | column-gap: 1rem;
1024 | }
1025 |
1026 | .md\:py-24 {
1027 | padding-top: 6rem;
1028 | padding-bottom: 6rem;
1029 | }
1030 | }
1031 |
1032 | @media (min-width: 1024px) {
1033 | .lg\:w-2\/3 {
1034 | width: 66.666667%;
1035 | }
1036 | }
--------------------------------------------------------------------------------