Commit

Author:

Hash:

Timestamp:

+1362 -445 +/-46 browse

Kevin Schoon [me@kevinschoon.com]

bbdaa91e2b03edc1787595657c186f28b23d5cd2

Tue, 17 Jun 2025 12:33:59 +0000 (1 week ago)

init promotional website, add project spec
1diff --git a/README.md b/README.md
2index cd3da6e..d9b5f78 100644
3--- a/README.md
4+++ b/README.md
5 @@ -1,451 +1,14 @@
6- # ForgeFeed
7+ # Public Website of [forge-feed.org](https://forge-feed.org)
8
9- *NOTE: This specification is a W.I.P and should not yet be implemented
10- anywhere.*
11+ ## Usage
12
13- ForgeFeed is a collection of specifications and recommendations which when
14- implemented can enhance interoperability and content discovery of different
15- [code forges](https://en.wikipedia.org/wiki/Forge_(software))
16- across the internet.
17+ You'll need to install [zola](https://www.getzola.org).
18
19- Below we cover the following specifications:
20-
21- Repository URI - URI Identifier for WebFinger Queries
22- WebFinger Query - Metadata Queries via WebFinger
23- RSS Specification - Repository Discovery via RSS
24-
25-
26- ## Repository URI
27-
28- A repository URI identifies a code repository and optionally the host that is
29- resides on. This value is similar to
30- [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565). The slug and hostname
31- part MUST match the URI path specification as defined in
32- [RFC3986-3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) while the
33- hostname, if specified, must match
34- [RFC3986-3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2)
35-
36- repository-uri = prefix slug hostname
37-
38- prefix = "repository:"
39- slug = rfc3986-path
40- hostname = [@ rfc3986-hostname]
41-
42- ### Slug
43-
44- The Slug represents a unique string that identifies a repository at a
45- particular code forge. Repository refers to a VCS managed codebase of some
46- kind, e.g. a Git repository.
47-
48- ### Hostname
49-
50- If the hostname part is missing then the address of the server receiving the
51- query is assumed. For example the following two queries are equivalent:
52-
53- ```text
54- https://example.org/.well-known/webfinger?resource=repository:example/spartacus
55- https://example.org/.well-known/webfinger?resource=repository:example/spartacus@example.org
56- ```
57-
58- TODO: Make this an actual spec, for now, some Python:
59-
60- ```python
61- from urllib.parse import urlparse, quote_plus
62-
63- def from_string(text):
64- url = urlparse(text)
65- if not url.path:
66- return (None, None)
67- split = url.path.split("@", 1)
68- if len(split) == 2:
69- return (split[0], split[1])
70- return (split[0], None)
71-
72- def to_string(slug, domain=None):
73- if domain:
74- return quote_plus(f"repository:{slug}@{domain}")
75- else:
76- return quote_plus(f"repository:{slug}")
77- ```
78-
79-
80- ## WebFinger Query
81-
82- A [WebFinger](https://webfinger.net/spec/) query may be used to identify
83- detailed information about a public repository at a particular forge. Here is
84- an example response about a fictitious repository:
85-
86- `GET https://example.org/.well-known/webfinger?resource=repository:example/spartacus`
87-
88- ```json
89- {
90- "subject": "repository:example/spartacus",
91- "aliases": [
92- "https://example.org/example/spartacus"
93- ],
94- "links": [
95- {
96- "rel": "http://webfinger.net/rel/avatar",
97- "href": "https://example.org/stylized-logo.png"
98- },
99- {
100- "rel": "http://forge-feed.org/rel/description",
101- "titles": {
102- "en-us": "A Text Adventure Written in FORTRAN 77",
103- "es": "Una Aventura de Texto Escrita en FORTRAN 77"
104- }
105- },
106- {
107- "rel": "http://feed-forge.org/rel/clone",
108- "href": "https://example.org/example/spartacus",
109- "properties": {
110- "http://feed-forge.org/ns/vcs-type": "git"
111- }
112- },
113- {
114- "rel": "http://forge-feed.org/rel/license",
115- "href": "https://example.com/example/spartacus/tree/LICENSE",
116- "properties": {
117- "spdx-identifier": "GPL-2.0-or-later"
118- }
119- },
120- {
121- "rel": "http://forge-feed.org/rel/chatroom",
122- "href": "ircs://irc.libera.chat/#spartacus-game"
123- "properties": {
124- "http://feed-forge.org/ns/chatroom": "irc"
125- }
126- },
127- {
128- "rel": "http://forge-feed.org/rel/label",
129- "properties": {
130- "http://feed-forge.org/ns/label": "fortran"
131- }
132- },
133- {
134- "rel": "http://forge-feed.org/rel/label",
135- "properties": {
136- "http://feed-forge.org/ns/label": "text-adventure"
137- }
138- }
139- ]
140- }
141- ```
142-
143- ### Properties
144-
145- #### http://feed-forge.org/ns/vcs-type
146-
147- Identifies VCS types, valid strings are:
148-
149- bzr (GNU Bazaar) bazaar.canonical.com
150- darcs (Darcs) darcs.net
151- fossil (Fossil) fossil-scm.org
152- git (Git) git-scm.com
153- hg (Mercurial) mercurial-scm.org
154- pijul (Pijul) pijul.org
155- svn (Apache Subversion) subversion.apache.org
156-
157- #### http://feed-forge.org/ns/chatroom
158-
159- Hint describing the backing type of chatroom. See [uri-schemes](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml).
160-
161- matrix
162- irc
163- xmpp
164-
165- #### http://feed-forge.org/ns/spdx-identifier
166-
167- Refers to a valid SPDX identifier, see [license-list](https://spdx.org/licenses/)
168-
169- ### Link Extension Types
170-
171- #### Avatar
172-
173- Forges that allow users to configure a logo can expose this information as
174- an avatar for use in other applications.
175-
176- ```json
177- {
178- "rel": "http://webfinger.net/rel/avatar",
179- "href": "https://example.org/stylized-logo.png"
180- }
181- ```
182-
183- #### Homepage
184-
185- Link to an HTTP representation of the project codebase
186-
187- ```json
188- {
189- "rel": "http://feed-forge.org/rel/homepage",
190- "href": "https://example.org/example/spartacus"
191- }
192- ```
193-
194- #### Description
195-
196- A short text representation of the repository.
197-
198- ```json
199- {
200- "rel": "http://example.org/rel/description",
201- "titles": {
202- "en-us": "A Text Adventure Written in FORTRAN 77",
203- "es": "Una Aventura de Texto Escrita en FORTRAN 77"
204- }
205- }
206- ```
207-
208- #### License
209-
210- A license SPDX identifier and link to the license's full text.
211-
212- ```json
213- {
214- "rel": "http://feed-forge.org/rel/license",
215- "href": "https://example.com/example/spartacus/tree/LICENSE",
216- "properties": {
217- "http://feed-forge.org/ns/spdx-identifier": "GPL-2.0-or-later"
218- }
219- }
220- ```
221-
222- ### Clone Links
223-
224- Clone links define how one can download a copy of the remote
225- software onto their own server.
226-
227- ```json
228- {
229- "rel": "http://feed-forge.org/rel/clone",
230- "href": "https://example.org/example/spartacus",
231- "properties": {
232- "http://feed-forge.org/ns/vcs-type": "git"
233- }
234- }
235+ ```sh
236+ zola serve
237 ```
238
239- #### Chat Links
240-
241- Links to chatrooms: IRC, Matrix, XMPP, etc.
242-
243- ```json
244- {
245- "rel": "http://feed-forge.org/rel/chatroom",
246- "href": "ircs://irc.libera.chat/#spartacus-game"
247- "properties": {
248- "http://feed-forge.org/ns/chatroom": "irc"
249- }
250- },
251- {
252- "rel": "http://feed-forge.org/rel/chatroom",
253- "href": "matrix:r/spartacus-game#example.org",
254- "properties": {
255- "http://feed-forge.org/ns/chatroom": "matrix"
256- }
257- },
258- {
259- "rel": "http://feed-forge.org/rel/chatroom",
260- "href": "xmpp:example.org/spartacus-game",
261- "properties": {
262- "http://feed-forge.org/ns/chatroom": "xmpp"
263- }
264- }
265- ```
266-
267- #### Mailing Lists
268-
269- Links to associated mailing lists, forms, etc.
270-
271- ```json
272- {
273- "rel": "http://feed-forge.org/rel/mailing-list",
274- "href": "mailto://list-name@mail.example.org",
275- "properties": {
276- "http://feed-forge.org/ns/mailing-list-subscribe": "mailto://subscribe+list-name@mail.example.org",
277- "http://feed-forge.org/ns/mailing-list-unsubscribe": "mailto://unsubscribe+list-name@mail.example.org"
278- }
279- }
280- ```
281-
282-
283- #### Free-Form Tags
284-
285- Arbitrary, free-form tags.
286-
287- ```json
288- {
289- "rel": "http://example.org/rel/label",
290- "properties": {
291- "http://feed-forge.org/ns/label": "fortran"
292- }
293- },
294- {
295- "rel": "http://example.org/rel/label",
296- "properties": {
297- "label": "text-adventure"
298- }
299- }
300- ```
301-
302- ### Security
303-
304- Repositories which are not publicly available should not be identifiable by
305- making webfinger queries at all. A repository which is private MUST return
306- the same response as a repository which does not exist when making a webfinger
307- request.
308-
309- GET https://example.org/.well-known/webfinger?resource=repository:example/spartacus
310- 200
311- GET https://example.org/.well-known/webfinger?resource=repository:example/private-repository
312- 404
313- GET https://example.org/.well-known/webfinger?resource=repository:example/non-existent-repository
314- 404
315-
316- # RSS Feeds
317-
318- Your forge needs to expose an RSS feed in order for other peers to determine
319- what code repositories exist in your server. Your server should expose
320- repositories ordered by recent updates. The heuristic you use to determine
321- "recently updated" depends on your forge. The simplest way to determine recent
322- activity is likely by your VCS's concept of a "commit".
323-
324- ## Determine if a Host Supports Forge Feed
325-
326- In order to participate in ForgeFeed your forge MUST present an HTML link
327- element such as below at the root domain of your forge. For example,
328- `code.example.org` MUST have a link element present in it's html header:
329-
330- ```html
331- <html>
332- <head>
333- <title>My Forge</title>
334- <link
335- rel="alternate"
336- type="application/rss+xml"
337- title="Recent Forge Activity"
338- href="https://code.example.org/firehose.xml" />
339- <!-- index-url refers to the RSS link on the current webpage which contains forge updates -->
340- <meta name="forge-feed:index-url" content="https://code.example.org/firehose.xml"/>
341- </head>
342- <body>
343- ...
344- </body>
345- </html>
346- ```
347-
348- The link MAY have a title of "Recent Forge Activity" such that it uniquely
349- identifies this feed as being related to forge updates. A server MUST provide a
350- meta tag with the name forge-feed:index where the content of the tag matches
351- the href of an RSS link listed on this page which should be used for indexing
352- by external crawlers. Applications which rely on the forge-feed:index meta
353- tag MUST stop crawling the serving website if this tag is no longer present
354- in the pages HTML.
355-
356- ### An example Feed
357-
358- Below is an example RSS feed with an optional repository section (described
359- below).
360-
361- ```xml
362- <?xml version="1.0" encoding="utf-8"?>
363- <rss version="2.0">
364- <channel>
365- <title>Firehose</title>
366- <link>https://code.example.org/browse</link>
367- <description>Recent Activity @ code.example.org</description>
368- <lastBuildDate>Tue, 03 Jun 2025 14:18:48 +0000</lastBuildDate>
369- <ttl>60</ttl>
370- <item>
371- <title>Spartacus</title>
372- <description>A Text Adventure Written in FORTRAN 77<description/>
373- <author>admin@example.org (Administrator)</author>
374- <!-- Link to an HTTP representation of the repository -->
375- <link>https://example.org/example/spartacus</link>
376- <!-- Guid may be the repository name at a particular commit id-->
377- <guid>https://example.org/example/spartacus@f28ce68f16e338355e55372bc788e8ea5feda470</guid>
378- <pubDate>Sun, 25 May 2025 18:23:03 +0200</pubDate>
379- <!-- Optional enclosure with an image reference if your forge supports this -->
380- <enclosure url="https://example.org/example/spartacus/raw/main/logo.png" length="12216320" type="image/png" />
381- <!-- Although highly recommended, the fields below are optional -->
382- <repository>
383- <!-- repository URI described above -->
384- <webfinger>repository:example/spartacus</webfinger>
385- </repository>
386- </item>
387- </channel>
388- </rss>
389- ```
390-
391- ### Repository Extension
392-
393- In order to facilitate discovery by external indexes it is highly recommended
394- that your server implement the webfinger repository specification described
395- above so that repository indexes may populate their state with rich information
396- about code repositories hosted on your server.
397-
398- ```xml
399- <repository>
400- <webfinger>repository:example/spartacus@example.org</webfinger>
401- </repository>
402- ```
403-
404- If the host section of the URI is included that is MUST match domain name
405- of the server which is providing the feed.
406-
407- ### Security Concerns
408-
409- #### Private Repositories
410-
411- Forge-feed enabled RSS feeds have no support for sharing private repositories
412- and any repository that is not shared publicly on the internet must be hidden
413- from the RSS activity feed stream. If your forge provides the ability to change
414- a repository from public to private it must be understood that clients may
415- already have cached versions of your repository data.
416-
417- ### Recommendations for Enumerating Repository Events
418-
419- #### Specify a Maximum Timeframe
420-
421- Your activity feed should not include repository events that are older than 1
422- week.
423-
424- #### Repository Items SHOULD be Unique
425-
426- Although it is permitted to return duplicate repositories in the feed forges
427- SHOULD only return unique repositories in a given window.
428-
429- #### Event "Clamping"
430-
431- It may be undesirable to enumerate repository events items with the simple
432- heuristic of
433-
434- end = time.now()
435- start = end - 6h
436-
437- events = repository_events_between(start, end)
438-
439- Because the oldest repository events will fall out of the time window on
440- subsequent queries by RSS readers. This can cause some RSS readers to
441- frequently request new content from your feed. Instead events can be
442- "clamped" within a certain time period. For example you may choose to
443- publish four buckets of updates per day:
444-
445- 00:00:00 06:00:00
446- 06:00:00 12:00:00
447- 12:00:00 18:00:00
448- 18:00:00 00:00:00
449-
450- For example if the current time is 2021-03-05 13:00:55 UTC you could use
451- the following example to return a summary of repository events from the
452- previous time bucket.
453-
454- now = time.now()
455- start = now.set_time("06:00:00")
456- end = now.set_time("12:00:00")
457-
458- events = repository_events_between(start, end)
459+ ### Attributions
460
461- This approach with a TTL value of 60 (minutes) will reduce excess requests
462- from some readers but allow all clients to receive timely updates.
463+ Theme is modified from [Minimal](https://github.com/semanticdata/zola-minimal/) by
464+ Miguel Pimentel.
465 diff --git a/config.toml b/config.toml
466new file mode 100644
467index 0000000..14a4082
468--- /dev/null
469+++ b/config.toml
470 @@ -0,0 +1,18 @@
471+ # The URL the site will be built for
472+ base_url = "https://forge-feed.org"
473+
474+ # Whether to automatically compile all Sass files in the sass directory
475+ compile_sass = true
476+
477+ # Whether to build a search index to be used later on by a JavaScript library
478+ build_search_index = false
479+
480+ description = "ForgeFeed Specifications"
481+
482+ [markdown]
483+ # Whether to do syntax highlighting
484+ # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola
485+ highlight_code = true
486+
487+ [extra]
488+ # Put all your custom variables here
489 diff --git a/content/_index.md b/content/_index.md
490new file mode 100644
491index 0000000..e5f1e2a
492--- /dev/null
493+++ b/content/_index.md
494 @@ -0,0 +1,21 @@
495+ ---
496+ title: Forge-Feed
497+ ---
498+
499+ **Please note that these specifications are currently PROVISIONAL
500+ and should not be implemented.**
501+
502+ ForgeFeed is a collection of specifications and recommendations which when
503+ implemented can enhance interoperability and content discovery of different
504+ [code forges](https://en.wikipedia.org/wiki/Forge_(software))
505+ across the internet.
506+
507+ NOTE that all specifications provided below MAY be implemented indepedenantly
508+ at the descrescion of the developer however it is a recommended that a server
509+ implement them all for maximum effect.
510+
511+ The following specifications are currently covered:
512+
513+ * [WebFinger Project Identification](/webfinger-project) - Identify Software Projects via WebFinger
514+ * [WebFinger Repository Identification](/webfinger-repository) - Identify VCS Repository Content via WebFinger
515+ * [Atom Feed Project Discovery](/project-atom-feed) - Subscribe to a forge over Atom
516 diff --git a/content/project-atom-feed.md b/content/project-atom-feed.md
517new file mode 100644
518index 0000000..4f55b97
519--- /dev/null
520+++ b/content/project-atom-feed.md
521 @@ -0,0 +1,145 @@
522+ ---
523+ title: Atom Feed
524+ date: 2023-11-17
525+ ---
526+
527+ # Atom Feeds
528+
529+ Your forge should expose an [Atom](https://www.rfc-editor.org/rfc/rfc4287)
530+ feed in order for other peers to subscribe to interesting code projects that
531+ are developed on your forge. The server should expose projects ordered by those
532+ which have been recently updated. THe heuristic you use to determine what has
533+ been updated depends on your forge. A common way to do this may be to simply
534+ look at your VCS's concept of a commit and order updates with that.
535+
536+ ## Determine if a Host Supports Forge Feed
537+
538+ In order to participate in ForgeFeed your forge MUST present an HTML link
539+ element such as below at the root domain of your forge. For example,
540+ `code.example.org` MUST have a link element present in it's html header:
541+
542+ ```html
543+ <html>
544+ <head>
545+ <title>My Forge</title>
546+ <link
547+ rel="alternate"
548+ type="application/atom+xml"
549+ title="Recent Forge Activity"
550+ href="https://code.example.org/firehose.xml" />
551+ <!-- index-url refers to the RSS link on the current webpage which contains forge updates -->
552+ <meta name="forge-feed:index-url" content="https://code.example.org/firehose.xml"/>
553+ </head>
554+ <body>
555+ ...
556+ </body>
557+ </html>
558+ ```
559+
560+ The link MAY have a title of "Recent Forge Activity" such that it uniquely
561+ identifies this feed as being related to forge updates. A server MUST provide a
562+ meta tag with the name forge-feed:index where the content of the tag matches
563+ the href of an Atom link listed on this page which should be used for indexing
564+ by external crawlers. Applications which rely on the forge-feed:index meta
565+ tag MUST stop crawling the serving website if this tag is no longer present
566+ in the pages HTML.
567+
568+ ### An example Feed
569+
570+ Below is an example Atom feed with an optional repository section (described below).
571+
572+ ```xml
573+ <?xml version="1.0" encoding="utf-8"?>
574+ <feed xmlns="http://www.w3.org/2005/Atom"
575+ xmlns:forge-feed="http://forge-feed.org/project-atom-feed">
576+
577+ <title>Acme Forge Firehose</title>
578+ <subtitle>Recent Project Updates @ Acme Forge</subtitle>
579+ <link href="https://code.example.org/firehose.xml" rel="self" />
580+ <link href="https://code.example.org/" />
581+ <id>urn:uuid:44decbfc-ec17-42fa-994e-67dc13029072</id>
582+ <updated>2025-06-18T11:23:02Z</updated>
583+
584+ <entry>
585+ <title>Spartacus Game</title>
586+ <link href="https://code.example.org/spartacus" />
587+ <id>urn:uuid:51b2ad2b-34ff-4a69-9f45-a300bf296d05</id>
588+ <published>2025-06-18T11:23:02Z</published>
589+ <updated>2025-06-18T11:23:02Z</updated>
590+ <summary>A Game Engine & Text Adventure Written in FORTRAN 77</summary>
591+ <forge-feed:project>project:spartacus</forge-feed:project>
592+ </entry>
593+ </feed>
594+ ```
595+
596+ ### ForgeFeed Extension
597+
598+ In order to facilitate discovery by external indexes it is highly recommended
599+ that your server implement the webfinger [project specification](/webfinger-project)
600+ so that repository indexes may populate their state with rich information
601+ about code repositories hosted on your server.
602+
603+ The extension currently supports only a single field called `project`.
604+
605+ ```xml
606+ <forge-feed:project>project:spartacus</forge-feed:project>
607+ ```
608+
609+ If the host section of the URI is included that is MUST match domain name
610+ of the server which is providing the feed.
611+
612+ ### Security Concerns
613+
614+ #### Private Projects
615+
616+ Forge-feed enabled Atom feeds have no support for sharing private projects
617+ and any project that is not shared publicly on the internet must be hidden
618+ from the Atom activity feed stream. If your forge provides the ability to change
619+ a project from public to private it must be understood that clients may
620+ already have cached versions of your project data.
621+
622+ ### Recommendations for Enumerating Project Events
623+
624+ #### Specify a Maximum Timeframe
625+
626+ Your activity feed should not include project events that are older than 1
627+ week.
628+
629+ #### Project Items SHOULD be Unique
630+
631+ Although it is permitted to return duplicate projects the feed SHOULD only
632+ return unique projects in a given window.
633+
634+ #### Event "Clamping"
635+
636+ It may be undesirable to enumerate project events items with the simple
637+ heuristic of
638+
639+ end = time.now()
640+ start = end - 6h
641+
642+ events = project_events_between(start, end)
643+
644+ Because the oldest project events will fall out of the time window on
645+ subsequent queries by feed readers. This can cause some feed readers to
646+ frequently request new content from your server. Instead events can be
647+ "clamped" within a certain time period. For example you may choose to
648+ publish four buckets of updates per day:
649+
650+ 00:00:00 06:00:00
651+ 06:00:00 12:00:00
652+ 12:00:00 18:00:00
653+ 18:00:00 00:00:00
654+
655+ For example if the current time is 2021-03-05 13:00:55 UTC you could use
656+ the following example to return a summary of project events from the
657+ previous time bucket.
658+
659+ now = time.now()
660+ start = now.set_time("06:00:00")
661+ end = now.set_time("12:00:00")
662+
663+ events = project_events_between(start, end)
664+
665+ This approach with a TTL value of 60 (minutes) will reduce excess requests
666+ from some readers but allow all clients to receive timely updates.
667 diff --git a/content/webfinger-project.md b/content/webfinger-project.md
668new file mode 100644
669index 0000000..b419c0c
670--- /dev/null
671+++ b/content/webfinger-project.md
672 @@ -0,0 +1,293 @@
673+ ---
674+ title: Project Discovery via WebFinger
675+ ---
676+
677+ A project refers to a software project which may contain multiple repositories
678+ as well as other resources such as mailing lists, bug trackers, links to
679+ chatrooms, etc. NOTE that some forges do not make a distinction between a
680+ project and a repository. If your forge does not then you MAY choose to expose
681+ repositories as a project with a single repository resource link.
682+
683+ # Project URI
684+
685+ A project URI identifies a software project and optionally the host that is
686+ resides on. This value is similar to
687+ [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565). The slug and hostname
688+ part MUST match the URI path specification as defined in
689+ [RFC3986-3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) while the
690+ hostname, if specified, must match
691+ [RFC3986-3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2).
692+
693+ project-uri = prefix slug hostname
694+
695+ prefix = "project:"
696+ slug = rfc3986-path
697+ hostname = [@ rfc3986-hostname]
698+
699+ ### Slug
700+
701+ The Slug represents a unique string that identifies a project at a particular code forge.
702+
703+ ### Hostname
704+
705+ If the hostname part is missing then the address of the server receiving the
706+ query is assumed. For example the following two queries are equivalent:
707+
708+ ```text
709+ https://example.org/.well-known/webfinger?resource=project:spartacus
710+ https://example.org/.well-known/webfinger?resource=project:spartacus@example.org
711+ ```
712+
713+ TODO: Make this an actual spec, for now, some Python:
714+
715+ ```python
716+ from urllib.parse import urlparse, quote_plus
717+
718+ def from_string(text):
719+ url = urlparse(text)
720+ if not url.path:
721+ return (None, None)
722+ split = url.path.split("@", 1)
723+ if len(split) == 2:
724+ return (split[0], split[1])
725+ return (split[0], None)
726+
727+ def to_string(slug, domain=None):
728+ if domain:
729+ return quote_plus(f"project:{slug}@{domain}")
730+ else:
731+ return quote_plus(f"project:{slug}")
732+ ```
733+
734+
735+ ### Properties
736+
737+ #### http://forge-feed.org/rel/repository
738+
739+ Reference to a VCS managed code repository.
740+
741+ #### http://feed-forge.org/ns/chatroom
742+
743+ Hint describing the backing type of chatroom. See [uri-schemes](https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml).
744+
745+ matrix
746+ irc
747+ xmpp
748+
749+ ### Link Extension Types
750+
751+ #### Avatar
752+
753+ Forges that allow users to configure a logo can expose this information as
754+ an avatar for use in other applications.
755+
756+ ```json
757+ {
758+ "rel": "http://webfinger.net/rel/avatar",
759+ "href": "https://example.org/stylized-logo.png"
760+ }
761+ ```
762+
763+ #### Homepage
764+
765+ Link to an HTTP representation of the project homepage.
766+
767+ ```json
768+ {
769+ "rel": "http://feed-forge.org/rel/homepage",
770+ "href": "https://example.org/example/spartacus"
771+ }
772+ ```
773+
774+ #### Description
775+
776+ A short text representation of the project.
777+
778+ ```json
779+ {
780+ "rel": "http://example.org/rel/description",
781+ "titles": {
782+ "en-us": "A Text Adventure Written in FORTRAN 77",
783+ "es": "Una Aventura de Texto Escrita en FORTRAN 77"
784+ }
785+ }
786+ ```
787+
788+ #### Chat Links
789+
790+ Links to chatrooms: IRC, Matrix, XMPP, etc.
791+
792+ ```json
793+ {
794+ "rel": "http://feed-forge.org/rel/chatroom",
795+ "href": "ircs://irc.libera.chat/#spartacus-game",
796+ "properties": {
797+ "http://feed-forge.org/ns/chatroom": "irc"
798+ }
799+ }
800+ ```
801+
802+ #### Mailing Lists
803+
804+ Links to associated mailing lists, forms, etc.
805+
806+ ```json
807+ {
808+ "rel": "http://feed-forge.org/rel/mailing-list",
809+ "href": "mailto://list-name@mail.example.org",
810+ "properties": {
811+ "http://feed-forge.org/ns/mailing-list-subscribe": "mailto://subscribe+list-name@mail.example.org",
812+ "http://feed-forge.org/ns/mailing-list-unsubscribe": "mailto://unsubscribe+list-name@mail.example.org"
813+ }
814+ }
815+ ```
816+
817+ #### Ticketing Systems
818+
819+ Links to issue tracking systems.
820+
821+ ```json
822+ {
823+ "ref": "http://forge-feed.org/rel/ticketing-system",
824+ "href": "https://example.org/bugs",
825+ }
826+ ```
827+
828+ #### Repository Links
829+
830+ Links of code repositories associated with this software project.
831+
832+ ```json
833+ {
834+ "rel": "http://example.org/rel/repository",
835+ "href": "https://example.org/example/spartacus",
836+ "properties": {
837+ "http://forge-feed.org/rel/repository": "repository:example/spartacus"
838+ }
839+ }
840+ ```
841+
842+ ## Example Multi-Repository Query
843+
844+ A [WebFinger](https://webfinger.net/spec/) query may be used to identify
845+ detailed information about a public repository at a particular forge. Here is
846+ an example response about a fictitious project which has two code repositories
847+ associated with it as well as chat links, and a bug tracking system.
848+
849+ `GET https://example.org/.well-known/webfinger?resource=project:spartacus`
850+
851+ ```json
852+ {
853+ "subject": "project:spartacus",
854+ "aliases": [
855+ "https://example.org"
856+ ],
857+ "links": [
858+ {
859+ "rel": "http://webfinger.net/rel/avatar",
860+ "href": "https://example.org/stylized-logo.png"
861+ },
862+ {
863+ "rel": "http://feed-forge.org/rel/homepage",
864+ "href": "https://code.example.org/spartacus"
865+ },
866+ {
867+ "rel": "http://forge-feed.org/rel/description",
868+ "titles": {
869+ "en-us": "A Text Adventure Written in FORTRAN 77",
870+ "es": "Una Aventura de Texto Escrita en FORTRAN 77"
871+ }
872+ },
873+ {
874+ "rel": "http://forge-feed.org/rel/chatroom",
875+ "href": "ircs://irc.libera.chat/#spartacus-game",
876+ "properties": {
877+ "http://feed-forge.org/ns/chatroom": "irc"
878+ }
879+ },
880+ {
881+ "rel": "http://forge-feed.org/rel/ticketing-system",
882+ "href": "https://example.org/bugs"
883+ },
884+ {
885+ "rel": "http://forge-feed.org/rel/label",
886+ "properties": {
887+ "http://feed-forge.org/ns/label": "fortran"
888+ }
889+ },
890+ {
891+ "rel": "http://forge-feed.org/rel/label",
892+ "properties": {
893+ "http://feed-forge.org/ns/label": "text-adventure"
894+ }
895+ },
896+ {
897+ "rel": "http://forge-feed.org/rel/repository",
898+ "href": "https://code.example.org/spartacus/spartan-engine",
899+ "titles": {
900+ "en-us": "The Spartan Game Engine"
901+ },
902+ "properties": {
903+ "http://forge-feed.org/rel/repository-uri": "repository:spartacus/game-engine"
904+ }
905+ },
906+ {
907+ "rel": "http://forge-feed.org/rel/repository",
908+ "href": "https://code.example.org/spartacus/game",
909+ "titles": {
910+ "en-us": "Implementation of the Spartacus Text Adventure game"
911+ },
912+ "properties": {
913+ "http://forge-feed.org/rel/repository-uri": "repository:spartacus/game"
914+ }
915+ },
916+ {
917+ "rel": "http://forge-feed.org/rel/repository",
918+ "href": "https://code.example.org/spartacus/game",
919+ "titles": {
920+ "en-us": "Promotional Website for the Spartacus Game"
921+ },
922+ "properties": {
923+ "http://forge-feed.org/rel/repository-uri": "repository:spartacus/www"
924+ }
925+ }
926+ ]
927+ }
928+ ```
929+
930+ ## Example Single Repository Query
931+
932+ Here is an example of a project with only a single repository associated with it.
933+
934+ ```json
935+ {
936+ "subject": "project:fuu/bar",
937+ "links": [
938+ {
939+ "rel": "http://forge-feed.org/rel/repository",
940+ "href": "https://code.example.org/fuu/bar",
941+ "titles": {
942+ "en-us": "Baz Qux"
943+ },
944+ "properties": {
945+ "http://forge-feed.org/rel/repository-uri": "repository:fuu/bar"
946+ }
947+ }
948+ ]
949+ }
950+ ```
951+
952+ ### Security
953+
954+ Projects which are not publicly available should not be identifiable by
955+ making webfinger queries at all. A project which is private MUST return
956+ the same response as a repository which does not exist when making a webfinger
957+ request.
958+
959+
960+ GET https://example.org/.well-known/webfinger?resource=project:example/spartacus
961+ 200
962+ GET https://example.org/.well-known/webfinger?resource=project:example/private-project
963+ 404
964+ GET https://example.org/.well-known/webfinger?resource=project:example/non-existent-project
965+ 404
966 diff --git a/content/webfinger-repository.md b/content/webfinger-repository.md
967new file mode 100644
968index 0000000..26c75e0
969--- /dev/null
970+++ b/content/webfinger-repository.md
971 @@ -0,0 +1,215 @@
972+ ---
973+ title: Repository Discovery via WebFinger
974+ ---
975+
976+ A repository refers to a version control managed source code which may be
977+ browsed over HTTP or downloaded with specific tooling.
978+
979+ ## Repository URI
980+
981+ A repository URI identifies a code repository and optionally the host that is
982+ resides on. This value is similar to
983+ [RFC7565](https://datatracker.ietf.org/doc/html/rfc7565). The slug and hostname
984+ part MUST match the URI path specification as defined in
985+ [RFC3986-3.3](https://www.rfc-editor.org/rfc/rfc3986#section-3.3) while the
986+ hostname, if specified, must match
987+ [RFC3986-3.2.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.2.2)
988+
989+ repository-uri = prefix slug hostname
990+
991+ prefix = "repository:"
992+ slug = rfc3986-path
993+ hostname = [@ rfc3986-hostname]
994+
995+ ### Slug
996+
997+ The Slug represents a unique string that identifies a repository at a
998+ particular code forge. Repository refers to a VCS managed codebase of some
999+ kind, e.g. a Git repository.
1000+
1001+ ### Hostname
1002+
1003+ If the hostname part is missing then the address of the server receiving the
1004+ query is assumed. For example the following two queries are equivalent:
1005+
1006+ ```text
1007+ https://example.org/.well-known/webfinger?resource=repository:spartacus/game
1008+ https://example.org/.well-known/webfinger?resource=repository:spartacus/game@example.org
1009+ ```
1010+
1011+ TODO: Make this an actual spec, for now, some Python:
1012+
1013+ ```python
1014+ from urllib.parse import urlparse, quote_plus
1015+
1016+ def from_string(text):
1017+ url = urlparse(text)
1018+ if not url.path:
1019+ return (None, None)
1020+ split = url.path.split("@", 1)
1021+ if len(split) == 2:
1022+ return (split[0], split[1])
1023+ return (split[0], None)
1024+
1025+ def to_string(slug, domain=None):
1026+ if domain:
1027+ return quote_plus(f"repository:{slug}@{domain}")
1028+ else:
1029+ return quote_plus(f"repository:{slug}")
1030+ ```
1031+
1032+ ### Properties
1033+
1034+ #### http://feed-forge.org/ns/vcs-type
1035+
1036+ Identifies VCS types, valid strings are:
1037+
1038+ bzr (GNU Bazaar) bazaar.canonical.com
1039+ darcs (Darcs) darcs.net
1040+ fossil (Fossil) fossil-scm.org
1041+ git (Git) git-scm.com
1042+ hg (Mercurial) mercurial-scm.org
1043+ pijul (Pijul) pijul.org
1044+ svn (Apache Subversion) subversion.apache.org
1045+
1046+ #### http://feed-forge.org/ns/spdx-identifier
1047+
1048+ Refers to a valid SPDX identifier, see [license-list](https://spdx.org/licenses/)
1049+
1050+ ### Link Extension Types
1051+
1052+ #### Avatar
1053+
1054+ Forges that allow users to configure a logo can expose this information as
1055+ an avatar for use in other applications.
1056+
1057+ ```json
1058+ {
1059+ "rel": "http://webfinger.net/rel/avatar",
1060+ "href": "https://example.org/stylized-logo.png"
1061+ }
1062+ ```
1063+
1064+ #### Homepage
1065+
1066+ Link to an HTTP representation of the repository.
1067+
1068+ ```json
1069+ {
1070+ "rel": "http://feed-forge.org/rel/homepage",
1071+ "href": "https://example.org/spartacus/game"
1072+ }
1073+ ```
1074+
1075+ #### Description
1076+
1077+ A short text representation of the repository.
1078+
1079+ ```json
1080+ {
1081+ "rel": "http://example.org/rel/description",
1082+ "titles": {
1083+ "en-us": "A Text Adventure Written in FORTRAN 77",
1084+ "es": "Una Aventura de Texto Escrita en FORTRAN 77"
1085+ }
1086+ }
1087+ ```
1088+
1089+ #### License
1090+
1091+ A license SPDX identifier and link to the license's full text.
1092+
1093+ ```json
1094+ {
1095+ "rel": "http://feed-forge.org/rel/license",
1096+ "href": "https://example.com/example/spartacus/tree/LICENSE",
1097+ "properties": {
1098+ "http://feed-forge.org/ns/spdx-identifier": "GPL-2.0-or-later"
1099+ }
1100+ }
1101+ ```
1102+
1103+ #### Clone Links
1104+
1105+ Clone links define how one can download a copy of the remote
1106+ software onto their own server.
1107+
1108+ ```json
1109+ {
1110+ "rel": "http://feed-forge.org/rel/clone",
1111+ "href": "https://example.org/example/spartacus",
1112+ "properties": {
1113+ "http://feed-forge.org/ns/vcs-type": "git"
1114+ }
1115+ }
1116+ ```
1117+
1118+ ## WebFinger Query
1119+
1120+ A [WebFinger](https://webfinger.net/spec/) query may be used to identify
1121+ detailed information about a public repository at a particular forge. Here is
1122+ an example response about a fictitious repository:
1123+
1124+ `GET https://example.org/.well-known/webfinger?resource=repository:spartacus/game`
1125+
1126+ ```json
1127+ {
1128+ "subject": "repository:spartacus/game",
1129+ "aliases": [
1130+ "https://example.org/spartacus/game"
1131+ ],
1132+ "links": [
1133+ {
1134+ "rel": "http://webfinger.net/rel/avatar",
1135+ "href": "https://example.org/stylized-logo.png"
1136+ },
1137+ {
1138+ "rel": "http://forge-feed.org/rel/description",
1139+ "titles": {
1140+ "en-us": "A Text Adventure Written in FORTRAN 77",
1141+ "es": "Una Aventura de Texto Escrita en FORTRAN 77"
1142+ }
1143+ },
1144+ {
1145+ "rel": "http://feed-forge.org/rel/clone",
1146+ "href": "https://example.org/spartacus/game",
1147+ "properties": {
1148+ "http://feed-forge.org/ns/vcs-type": "git"
1149+ }
1150+ },
1151+ {
1152+ "rel": "http://forge-feed.org/rel/license",
1153+ "href": "https://example.com/spartacus/game/tree/LICENSE",
1154+ "properties": {
1155+ "spdx-identifier": "GPL-2.0-or-later"
1156+ }
1157+ },
1158+ {
1159+ "rel": "http://forge-feed.org/rel/label",
1160+ "properties": {
1161+ "http://feed-forge.org/ns/label": "fortran"
1162+ }
1163+ },
1164+ {
1165+ "rel": "http://forge-feed.org/rel/label",
1166+ "properties": {
1167+ "http://feed-forge.org/ns/label": "text-adventure"
1168+ }
1169+ }
1170+ ]
1171+ }
1172+ ```
1173+
1174+ ### Security
1175+
1176+ Repositories which are not publicly available should not be identifiable by
1177+ making webfinger queries at all. A repository which is private MUST return
1178+ the same response as a repository which does not exist when making a webfinger
1179+ request.
1180+
1181+ GET https://example.org/.well-known/webfinger?resource=repository:spartacus/game
1182+ 200
1183+ GET https://example.org/.well-known/webfinger?resource=repository:example/private-repository
1184+ 404
1185+ GET https://example.org/.well-known/webfinger?resource=repository:example/non-existent-repository
1186+ 404
1187 diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png
1188new file mode 100644
1189index 0000000..f6137eb
1190 Binary files /dev/null and b/public/android-chrome-192x192.png differ
1191 diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png
1192new file mode 100644
1193index 0000000..c77c2af
1194 Binary files /dev/null and b/public/android-chrome-512x512.png differ
1195 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png
1196new file mode 100644
1197index 0000000..aaeaa8d
1198 Binary files /dev/null and b/public/apple-touch-icon.png differ
1199 diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png
1200new file mode 100644
1201index 0000000..29dd09f
1202 Binary files /dev/null and b/public/favicon-16x16.png differ
1203 diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png
1204new file mode 100644
1205index 0000000..811ba56
1206 Binary files /dev/null and b/public/favicon-32x32.png differ
1207 diff --git a/public/favicon.ico b/public/favicon.ico
1208new file mode 100644
1209index 0000000..de83549
1210 Binary files /dev/null and b/public/favicon.ico differ
1211 diff --git a/public/img/codeberg.svg b/public/img/codeberg.svg
1212new file mode 100644
1213index 0000000..068702a
1214--- /dev/null
1215+++ b/public/img/codeberg.svg
1216 @@ -0,0 +1,19 @@
1217+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg-badge" height="168" width="564" viewBox="0 0 564 168">
1218+ <defs>
1219+ <linearGradient xlink:href="#a" id="b" gradientUnits="userSpaceOnUse" x1="42519.285" y1="-7078.789" x2="42575.336" y2="-6966.931" />
1220+ <linearGradient id="a">
1221+ <stop offset="0" stop-color="#fff" stop-opacity="0" />
1222+ <stop offset=".495" stop-color="#71c2ff" />
1223+ <stop offset="1" stop-color="#39aaff" />
1224+ </linearGradient>
1225+ </defs>
1226+ <rect id="rect" ry="15.111" y="2" x="2" height="164" width="560" fill="none" stroke="#a6a6a6" stroke-width="4" stroke-linejoin="round" paint-order="fill stroke markers" />
1227+ <text style="line-height:0%;-inkscape-font-specification:'Inter';marker:none" x="203.16" y="-85.813" transform="matrix(1.0087 0 0 .99138 -19.194 133.848)" color="#000" font-weight="400" font-size="11.814" font-family="Inter, 'DejaVu Sans', sans-serif" letter-spacing="0" word-spacing="0" overflow="visible" fill="#2185d0" stroke-width=".985">
1228+ <tspan id="claim-svg" style="line-height:1;-inkscape-font-specification:'Inter'" x="203.16" y="-85.813" font-size="33.596">GET IT ON</tspan>
1229+ </text>
1230+ <path aria-label="Codeberg" d="M214.654 71.02c-8.453 0-15.047 2.79-19.803 8.369-4.803 5.577-7.215 12.943-7.215 22.096 0 7.034 1.764 13.158 5.281 18.372 4.862 7.275 12.274 10.912 22.2 10.912 6.907 0 12.922-2.516 18.049-7.548l-4.718-7.185c-4.57 2.85-8.512 4.275-11.844 4.275-5.069 0-9.112-1.91-12.12-5.729-2.847-3.638-4.271-8.004-4.271-13.097 0-5.76 1.211-10.428 3.613-14.005 2.773-4.183 6.694-6.274 11.747-6.274 3.65 0 7.684 1.574 12.11 4.728l4.734-7.546c-5.925-4.91-11.844-7.368-17.763-7.368zm167.7 0l-11.657 6.456v52.201h9.441l.925-4.091c2.843 3.455 6.844 5.183 12.03 5.183 5.238 0 9.638-1.548 13.224-4.64 4.56-4.061 6.85-10.033 6.85-17.914 0-6.122-1.547-11.033-4.634-14.732-3.523-4.184-8.38-6.275-14.612-6.275-2.226 0-4.266.418-6.105 1.274-2.348 1.09-4.176 2.607-5.462 4.546zm-62.745.183l-11.674 6.367v13.186c-2.895-2.304-6.471-3.455-10.722-3.455-4.93 0-9.16 1.606-12.667 4.82-4.692 4.304-7.056 10.427-7.056 18.37 0 5.638 1.466 10.247 4.362 13.824 3.454 4.304 8.486 6.456 15.085 6.456 2.343 0 4.622-.517 6.838-1.547 2.232-1.03 3.83-2.334 4.825-3.909l1.663 4.364h9.346zm-65.647 16.098c-6.057 0-10.956 1.878-14.718 5.636-4.139 4.123-6.206 9.52-6.206 16.188 0 5.88 1.855 10.854 5.553 14.916 4.134 4.486 9.272 6.73 15.371 6.73 5.255 0 9.84-1.759 13.783-5.275 4.75-4.183 7.125-9.641 7.125-16.371 0-5.758-1.599-10.638-4.808-14.641-3.826-4.79-9.192-7.183-16.1-7.183zm91.746 0c-5.988 0-10.914 1.94-14.819 5.82-4.314 4.245-6.477 9.609-6.477 16.096 0 6.61 1.923 11.792 5.744 15.552 4.017 4 9.596 6.002 16.758 6.002 6.647 0 12.205-1.94 16.641-5.822l-2.964-6.092c-4.368 2.426-8.56 3.637-12.572 3.637-2.842 0-5.302-.908-7.401-2.728-2.051-1.88-3.194-4.214-3.417-7.001h28.852v-4.183c0-6.003-1.6-10.885-4.809-14.643-3.693-4.427-8.884-6.638-15.536-6.638zm92.543 0c-5.978 0-10.909 1.94-14.803 5.82-4.326 4.245-6.472 9.609-6.472 16.096 0 6.61 1.907 11.792 5.733 15.552 4 4 9.585 6.002 16.753 6.002 6.658 0 12.205-1.94 16.652-5.822l-2.954-6.092c-4.389 2.426-8.587 3.637-12.588 3.637-2.837 0-5.308-.908-7.406-2.728-2.04-1.88-3.183-4.214-3.427-7.001h28.878v-4.183c0-6.003-1.605-10.885-4.814-14.643-3.709-4.427-8.879-6.638-15.552-6.638zm45.163 0c-1.838 0-3.672.485-5.462 1.455-1.727.908-2.901 2-3.512 3.272l-1.11-3.635h-9.9v41.286h11.477V100.03c.978-2.12 2.524-3.18 4.623-3.18 2.28 0 4.442 1.15 6.466 3.454l9.814-5.274c-2.71-5.154-6.844-7.73-12.396-7.73zm30.121 0c-4.883 0-9.107 1.606-12.683 4.82-4.691 4.304-7.024 10.427-7.024 18.37 0 5.577 1.435 10.186 4.336 13.824 3.4 4.304 8.188 6.456 14.351 6.456 5.064 0 8.974-2.032 11.759-6.094 0 9.094-3.342 13.642-10 13.642-4.697 0-8.921-1.213-12.683-3.638l-2.954 5.82c4.856 4.365 10.637 6.549 17.295 6.549 6.413 0 11.354-1.85 14.813-5.549 3.502-3.758 5.266-9.125 5.255-16.098v-37.01h-8.974l-.914 4.455c0-.604-.893-1.515-2.688-2.728-2.716-1.879-6.005-2.82-9.889-2.82zm-167.923 8.094c5.553 0 8.496 2.97 8.794 8.912h-17.869c.675-5.942 3.704-8.912 9.075-8.912zm92.554 0c5.547 0 8.474 2.97 8.788 8.912h-17.858c.674-5.942 3.703-8.912 9.07-8.912zm-184.204.089c5.989 0 8.98 4.547 8.98 13.64 0 8.732-2.991 13.097-8.98 13.097-5.982 0-8.98-4.365-8.98-13.096 0-9.094 2.998-13.641 8.98-13.641zm45.748 0c2.04 0 3.794.67 5.282 2.002 1.53 1.272 2.518 2.908 2.943 4.909v11.55c-.318 2.243-1.408 4.243-3.315 6.002-1.913 1.698-3.985 2.547-6.2 2.547-5.989 0-8.98-4.516-8.98-13.55 0-4.123.924-7.396 2.778-9.82 1.85-2.426 4.347-3.64 7.492-3.64zm216.312 0c1.982 0 3.762.7 5.377 2.093 1.669 1.335 2.625 2.94 2.875 4.818v10.46c0 2.607-.935 4.878-2.785 6.82-1.854 1.878-4.043 2.819-6.572 2.819-6.047 0-9.075-4.516-9.075-13.55 0-4.123.924-7.396 2.779-9.82 1.849-2.426 4.325-3.64 7.401-3.64zm-124.322.094c2.832 0 5.048 1.211 6.663 3.637 1.594 2.425 2.407 5.728 2.407 9.912 0 4.244-.866 7.58-2.598 10.006-1.727 2.424-4.07 3.637-7.019 3.637-2.295 0-4.31-.76-6.03-2.274-1.68-1.518-2.604-3.396-2.769-5.637v-9.916c0-2.606.893-4.818 2.673-6.637 1.854-1.817 4.075-2.728 6.673-2.728z" style="line-height:1.25;-inkscape-font-specification:'Tajawal Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal" font-weight="700" font-size="17.899" font-family="Tajawal" letter-spacing="-.285" word-spacing="0" fill="#2185d0" />
1231+ <g paint-order="stroke markers fill">
1232+ <path style="font-variation-settings:normal" d="M42519.285-7078.79a.76.568 0 00-.738.675l33.586 125.888a87.182 87.182 0 0039.381-33.763l-71.565-92.52a.76.568 0 00-.664-.28z" transform="matrix(.75692 0 0 .74393 -32088.397 5317.32)" opacity=".5" fill="url(#b)" />
1233+ <path d="M93.742 20.95A65.99 64.857 0 0028 85.807a65.99 64.857 0 0010.076 34.447l55.019-69.909a1.03.756 0 011.785 0l55.022 69.912a65.99 64.857 0 0010.078-34.45A65.99 64.857 0 0093.99 20.95a65.99 64.857 0 00-.248 0z" fill="#2185d0" />
1234+ </g>
1235+ </svg>
1236\ No newline at end of file
1237 diff --git a/public/img/logo.png b/public/img/logo.png
1238new file mode 100644
1239index 0000000..93e608e
1240 Binary files /dev/null and b/public/img/logo.png differ
1241 diff --git a/public/img/logo.svg b/public/img/logo.svg
1242new file mode 100644
1243index 0000000..cdc0267
1244--- /dev/null
1245+++ b/public/img/logo.svg
1246 @@ -0,0 +1 @@
1247+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><g transform="translate(0 -852.36)"><rect width="180" height="180" x="10" y="862.36" rx="8.546" ry="7.52" style="fill-opacity:.24215;fill:#e7eab7"/><path d="M159.21 52.277c0 12.686-11.082 22.97-24.752 22.97s-24.752-10.284-24.752-22.97 11.082-22.97 24.752-22.97 24.752 10.284 24.752 22.97z" style="fill-opacity:.89686;fill:#b3b3b3" transform="matrix(.6464 0 0 .69655 -38.912 967.95)"/><path d="M118.91 1020.4c.102-.453.25-.888.344-1.344a72.568 72.568 0 0 0 1.094-7.219c.248-2.444.375-4.927.375-7.437s-.127-4.993-.375-7.438-.61-4.851-1.094-7.218a72.281 72.281 0 0 0-1.813-6.97 72.276 72.276 0 0 0-2.437-6.687c-.92-2.175-1.943-4.283-3.063-6.343s-2.318-4.066-3.625-6-2.705-3.798-4.187-5.594a73.377 73.377 0 0 0-4.72-5.156c-1.644-1.645-3.36-3.237-5.155-4.72s-3.66-2.88-5.594-4.187c-1.934-1.306-3.94-2.505-6-3.625s-4.169-2.142-6.344-3.062a72.255 72.255 0 0 0-6.687-2.438 72.245 72.245 0 0 0-6.97-1.812 72.57 72.57 0 0 0-7.218-1.094c-2.445-.248-4.927-.375-7.437-.375s-4.993.127-7.438.375-4.852.61-7.219 1.094c-.456.093-.89.242-1.344.344v25.5c1.704-.552 3.345-1.23 5.125-1.594 1.595-.327 3.228-.583 4.875-.75s3.309-.25 5-.25 3.353.082 5 .25c1.648.167 3.28.423 4.875.75 3.19.652 6.257 1.604 9.188 2.844s5.737 2.77 8.344 4.53a49.125 49.125 0 0 1 7.25 5.97 49.138 49.138 0 0 1 5.969 7.25 49.135 49.135 0 0 1 4.53 8.343 48.502 48.502 0 0 1 2.845 9.188c.326 1.595.582 3.228.75 4.875s.25 3.308.25 5c0 1.691-.083 3.353-.25 5a49.126 49.126 0 0 1-.75 4.875c-.365 1.78-1.043 3.42-1.594 5.125h25.5z" style="fill-opacity:.89686;fill:#b3b3b3"/><path d="M48 874.36c-4.487 0-8.911.212-13.281.656-.92.094-1.805.294-2.719.407v29.562c1.894-.303 3.76-.68 5.688-.875 3.388-.345 6.833-.532 10.312-.532 3.478 0 6.924.187 10.312.532 3.388.344 6.72.86 10 1.53a99.75 99.75 0 0 1 18.906 5.844c3.014 1.275 5.956 2.699 8.812 4.25s5.632 3.252 8.313 5.063 5.261 3.727 7.75 5.781 4.876 4.252 7.156 6.531 4.478 4.668 6.531 7.157c2.054 2.488 3.97 5.069 5.781 7.75s3.511 5.456 5.063 8.312 2.975 5.798 4.25 8.813 2.393 6.094 3.375 9.25a99.975 99.975 0 0 1 2.469 9.656c.671 3.28 1.187 6.612 1.531 10 .344 3.388.531 6.834.531 10.313 0 3.479-.187 6.924-.531 10.312-.196 1.927-.572 3.794-.875 5.687h29.562c.112-.914.313-1.799.406-2.718.444-4.37.656-8.794.656-13.281 0-4.488-.212-8.911-.656-13.281-.444-4.37-1.134-8.675-2-12.906a129.23 129.23 0 0 0-3.187-12.47c-1.266-4.07-2.73-8.049-4.375-11.937a129.601 129.601 0 0 0-11.969-22.094c-2.336-3.458-4.85-6.79-7.5-10s-5.434-6.31-8.375-9.25-6.04-5.726-9.25-8.375-6.542-5.164-10-7.5a129.885 129.885 0 0 0-22.094-11.97 128.88 128.88 0 0 0-11.938-4.374 129.205 129.205 0 0 0-12.469-3.188c-4.231-.866-8.536-1.556-12.906-2-4.349-.48-8.773-.69-13.26-.69z" style="fill-opacity:.89686;fill:#b3b3b3"/></g></svg>
1248\ No newline at end of file
1249 diff --git a/public/img/minimal-favicon.png b/public/img/minimal-favicon.png
1250new file mode 100644
1251index 0000000..e7cdbde
1252 Binary files /dev/null and b/public/img/minimal-favicon.png differ
1253 diff --git a/public/img/minimal-minimal.png b/public/img/minimal-minimal.png
1254new file mode 100644
1255index 0000000..8f60a03
1256 Binary files /dev/null and b/public/img/minimal-minimal.png differ
1257 diff --git a/public/img/minimal-thumbnail.png b/public/img/minimal-thumbnail.png
1258new file mode 100644
1259index 0000000..e6c0478
1260 Binary files /dev/null and b/public/img/minimal-thumbnail.png differ
1261 diff --git a/public/img/sourcehut.svg b/public/img/sourcehut.svg
1262new file mode 100644
1263index 0000000..c7a12e9
1264--- /dev/null
1265+++ b/public/img/sourcehut.svg
1266 @@ -0,0 +1,77 @@
1267+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1268+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
1269+
1270+ <svg
1271+ width="300"
1272+ height="80"
1273+ viewBox="0 0 79.375001 21.166666"
1274+ version="1.1"
1275+ id="svg5"
1276+ inkscape:version="1.1 (c68e22c387, 2021-05-23)"
1277+ sodipodi:docname="sourcehut-black.svg"
1278+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
1279+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
1280+ xmlns="http://www.w3.org/2000/svg"
1281+ xmlns:svg="http://www.w3.org/2000/svg">
1282+ <sodipodi:namedview
1283+ id="namedview7"
1284+ pagecolor="#505050"
1285+ bordercolor="#ffffff"
1286+ borderopacity="1"
1287+ inkscape:pageshadow="0"
1288+ inkscape:pageopacity="0"
1289+ inkscape:pagecheckerboard="1"
1290+ inkscape:document-units="px"
1291+ showgrid="false"
1292+ units="px"
1293+ height="80px"
1294+ inkscape:zoom="3.0930415"
1295+ inkscape:cx="152.6006"
1296+ inkscape:cy="77.75518"
1297+ inkscape:window-width="1920"
1298+ inkscape:window-height="1059"
1299+ inkscape:window-x="0"
1300+ inkscape:window-y="0"
1301+ inkscape:window-maximized="1"
1302+ inkscape:current-layer="layer1" />
1303+ <defs
1304+ id="defs2">
1305+ <rect
1306+ x="72.43578"
1307+ y="4.989779"
1308+ width="233.3009"
1309+ height="93.363552"
1310+ id="rect22993" />
1311+ <rect
1312+ x="76.460692"
1313+ y="7.5029808"
1314+ width="210.43456"
1315+ height="46.880685"
1316+ id="rect5098" />
1317+ </defs>
1318+ <g
1319+ inkscape:label="Layer 1"
1320+ inkscape:groupmode="layer"
1321+ id="layer1">
1322+ <path
1323+ d="m 10.58314,1.3501934 c -5.1005652,0 -9.23314,4.1325746 -9.23314,9.2331386 0,5.100566 4.1325748,9.233141 9.23314,9.233141 5.100565,0 9.23314,-4.132575 9.23314,-9.233141 0,-5.100564 -4.132575,-9.2331386 -9.23314,-9.2331386 z m 0,16.6792216 c -4.1139596,0 -7.4460807,-3.332121 -7.4460807,-7.446083 0,-4.1139582 3.3321211,-7.4460793 7.4460807,-7.4460793 4.11396,0 7.446082,3.3321211 7.446082,7.4460793 0,4.113962 -3.332122,7.446083 -7.446082,7.446083 z"
1324+ id="path49"
1325+ style="stroke-width:0.0372303" />
1326+ <text
1327+ xml:space="preserve"
1328+ transform="scale(0.26458333)"
1329+ id="text22991"
1330+ style="fill:black;fill-opacity:1;line-height:1.25;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;white-space:pre;shape-inside:url(#rect22993)" />
1331+ <text
1332+ xml:space="preserve"
1333+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2889px;line-height:1.25;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
1334+ x="22.349714"
1335+ y="13.896598"
1336+ id="text29885"><tspan
1337+ sodipodi:role="line"
1338+ id="tspan29883"
1339+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2889px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583"
1340+ x="22.349714"
1341+ y="13.896598">sourcehut</tspan></text>
1342+ </g>
1343+ </svg>
1344 diff --git a/public/site.webmanifest b/public/site.webmanifest
1345new file mode 100644
1346index 0000000..06d341a
1347--- /dev/null
1348+++ b/public/site.webmanifest
1349 @@ -0,0 +1,19 @@
1350+ {
1351+ "name": "",
1352+ "short_name": "",
1353+ "icons": [
1354+ {
1355+ "src": "/android-chrome-192x192.png",
1356+ "sizes": "192x192",
1357+ "type": "image/png"
1358+ },
1359+ {
1360+ "src": "/android-chrome-512x512.png",
1361+ "sizes": "512x512",
1362+ "type": "image/png"
1363+ }
1364+ ],
1365+ "theme_color": "#ffffff",
1366+ "background_color": "#ffffff",
1367+ "display": "standalone"
1368+ }
1369 diff --git a/public/styles.css b/public/styles.css
1370new file mode 100644
1371index 0000000..85b557a
1372--- /dev/null
1373+++ b/public/styles.css
1374 @@ -0,0 +1 @@
1375+ .wrapper{width:860px;margin:0 auto}header{width:170px;float:left;position:fixed;-webkit-font-smoothing:subpixel-antialiased}body{background-color:#fff;padding:50px;font:14px/1.5 "Noto Sans","Helvetica Neue",Helvetica,Arial,sans-serif;color:#222;font-weight:400}h1,h2,h3,h4,h5,h6{color:#222;margin:0 0 20px}p,ul,ol,table,pre,dl{margin:0 0 20px}h1,h2,h3{line-height:1.1}h1{font-size:28px}a{color:#267cb9;text-decoration:none}a:hover,a:focus{color:salmon}a small{font-size:11px;color:#000;margin-top:-.3em;display:block}a:hover small{color:#777}strong{color:#222;font-weight:700}section{width:600px;float:right;padding-bottom:50px}small{font-size:11px}hr{border:0;background:#e5e5e5;height:1px;margin:0 0 20px}footer{width:270px;float:left;position:fixed;bottom:50px;-webkit-font-smoothing:subpixel-antialiased}pre{padding:8px 15px;border-radius:5px;border:1px solid #e5e5e5;overflow-x:auto}ul.downloads{list-style:none;height:40px;padding:0;background:#f4f4f4;border-radius:5px;border:1px solid #e0e0e0;width:270px}.downloads li{width:89px;float:left;border-right:1px solid #e0e0e0;height:40px}.downloads li:first-child a{border-radius:5px 0 0 5px}.downloads li:last-child a{border-radius:0 5px 5px 0}.downloads a{line-height:1;font-size:11px;display:block;text-align:center;padding-top:6px;height:34px}.downloads a:hover,.downloads a:focus{font-weight:bold}.downloads ul a:active{background-color:#f0f0f0}.downloads li+li+li{border-right:none;width:89px}.downloads a strong{font-size:14px;display:block;color:#222}.site-logo{margin-bottom:1rem;width:100%}@media print,screen and (max-width: 960px){div.wrapper{width:auto;margin:0}header,section,footer{float:none;position:static;width:auto}header{padding-right:320px}section{border:1px solid #e5e5e5;border-width:1px 0;padding:20px 0;margin:0 0 20px}header a small{display:inline}header ul{position:absolute;right:50px;top:52px}}@media print,screen and (max-width: 720px){body{word-wrap:break-word}header{padding:0}header ul,header p.view{position:static;margin:1rem auto}pre,code{word-wrap:normal;text-wrap:wrap}}@media print,screen and (max-width: 480px){body{padding:15px}.downloads{width:100%;margin:1rem auto}.downloads li,.downloads li+li+li{width:33%}}@media print{body{padding:.4in;font-size:12pt;color:#444}}::selection{background:#639;color:#fff}table{margin:1rem auto;border-collapse:collapse}table thead th,table tfoot th{color:#202020;background:rgba(0,0,0,.15)}table caption{padding:.5em}table th,table td{padding:.5em;border:1px solid #e5e5e5}pre,code{font-family:monospace}code{border-radius:3px}pre{overflow:auto}pre code{background-color:rgba(0,0,0,0);color:inherit}@media (prefers-color-scheme: dark){.sourcehut{filter:invert(100%)}body{background-color:#222;color:#fff}strong{color:#fff}h1,h2,h3,h4,h5,h6{color:#fff}a small{color:#fff}}
1376\ No newline at end of file
1377 diff --git a/sass/_base.scss b/sass/_base.scss
1378new file mode 100644
1379index 0000000..073be69
1380--- /dev/null
1381+++ b/sass/_base.scss
1382 @@ -0,0 +1,114 @@
1383+ @use "variables";
1384+
1385+ .wrapper {
1386+ width: 860px;
1387+ margin: 0 auto;
1388+ }
1389+
1390+ header {
1391+ width: 170px;
1392+ float: left;
1393+ position: fixed;
1394+ -webkit-font-smoothing: subpixel-antialiased;
1395+ }
1396+
1397+ body {
1398+ background-color: #fff;
1399+ padding: 50px;
1400+ font:
1401+ 14px/1.5 "Noto Sans",
1402+ "Helvetica Neue",
1403+ Helvetica,
1404+ Arial,
1405+ sans-serif;
1406+ color: #222;
1407+ font-weight: 400;
1408+ }
1409+
1410+ h1,
1411+ h2,
1412+ h3,
1413+ h4,
1414+ h5,
1415+ h6 {
1416+ color: #222;
1417+ margin: 0 0 20px;
1418+ }
1419+
1420+ p,
1421+ ul,
1422+ ol,
1423+ table,
1424+ pre,
1425+ dl {
1426+ margin: 0 0 20px;
1427+ }
1428+
1429+ h1,
1430+ h2,
1431+ h3 {
1432+ line-height: 1.1;
1433+ }
1434+
1435+ h1 {
1436+ font-size: 28px;
1437+ }
1438+
1439+ a {
1440+ color: #267cb9;
1441+ text-decoration: none;
1442+ }
1443+
1444+ a:hover,
1445+ a:focus {
1446+ color: salmon;
1447+ }
1448+
1449+ a small {
1450+ font-size: 11px;
1451+ color: black;
1452+ margin-top: -0.3em;
1453+ display: block;
1454+ }
1455+
1456+ a:hover small {
1457+ color: #777;
1458+ }
1459+
1460+ strong {
1461+ color: #222;
1462+ font-weight: 700;
1463+ }
1464+
1465+ section {
1466+ width: 600px;
1467+ float: right;
1468+ padding-bottom: 50px;
1469+ }
1470+
1471+ small {
1472+ font-size: 11px;
1473+ }
1474+
1475+ hr {
1476+ border: 0;
1477+ background: #e5e5e5;
1478+ height: 1px;
1479+ margin: 0 0 20px;
1480+ }
1481+
1482+ footer {
1483+ width: 270px;
1484+ float: left;
1485+ position: fixed;
1486+ bottom: 50px;
1487+ -webkit-font-smoothing: subpixel-antialiased;
1488+ }
1489+
1490+ pre {
1491+ padding: 8px 15px;
1492+ // background: #f8f8f8;
1493+ border-radius: 5px;
1494+ border: 1px solid #e5e5e5;
1495+ overflow-x: auto;
1496+ }
1497 diff --git a/sass/_classes.scss b/sass/_classes.scss
1498new file mode 100644
1499index 0000000..a00bb4c
1500--- /dev/null
1501+++ b/sass/_classes.scss
1502 @@ -0,0 +1,58 @@
1503+ ul.downloads {
1504+ list-style: none;
1505+ height: 40px;
1506+ padding: 0;
1507+ background: #f4f4f4;
1508+ border-radius: 5px;
1509+ border: 1px solid #e0e0e0;
1510+ width: 270px;
1511+ }
1512+
1513+ .downloads li {
1514+ width: 89px;
1515+ float: left;
1516+ border-right: 1px solid #e0e0e0;
1517+ height: 40px;
1518+ }
1519+
1520+ .downloads li:first-child a {
1521+ border-radius: 5px 0 0 5px;
1522+ }
1523+
1524+ .downloads li:last-child a {
1525+ border-radius: 0 5px 5px 0;
1526+ }
1527+
1528+ .downloads a {
1529+ line-height: 1;
1530+ font-size: 11px;
1531+ display: block;
1532+ text-align: center;
1533+ padding-top: 6px;
1534+ height: 34px;
1535+ }
1536+
1537+ .downloads a:hover,
1538+ .downloads a:focus {
1539+ font-weight: bold;
1540+ }
1541+
1542+ .downloads ul a:active {
1543+ background-color: #f0f0f0;
1544+ }
1545+
1546+ .downloads li + li + li {
1547+ border-right: none;
1548+ width: 89px;
1549+ }
1550+
1551+ .downloads a strong {
1552+ font-size: 14px;
1553+ display: block;
1554+ color: #222;
1555+ }
1556+
1557+ .site-logo {
1558+ margin-bottom: 1rem;
1559+ width: 100%;
1560+ }
1561 diff --git a/sass/_code.scss b/sass/_code.scss
1562new file mode 100644
1563index 0000000..0dd3c6d
1564--- /dev/null
1565+++ b/sass/_code.scss
1566 @@ -0,0 +1,17 @@
1567+ pre,
1568+ code {
1569+ font-family: monospace;
1570+ }
1571+
1572+ code {
1573+ border-radius: 3px;
1574+ }
1575+
1576+ pre {
1577+ overflow: auto;
1578+ }
1579+
1580+ pre code {
1581+ background-color: transparent;
1582+ color: inherit;
1583+ }
1584 diff --git a/sass/_custom.scss b/sass/_custom.scss
1585new file mode 100644
1586index 0000000..213f60b
1587--- /dev/null
1588+++ b/sass/_custom.scss
1589 @@ -0,0 +1,30 @@
1590+ // Custom CSS code goes here
1591+
1592+ @media (prefers-color-scheme: dark) {
1593+
1594+ .sourcehut {
1595+ filter: invert(100%) ;
1596+ }
1597+
1598+ body {
1599+ background-color: #222;
1600+ color: #FFF
1601+ }
1602+
1603+ strong {
1604+ color: #FFF;
1605+ }
1606+
1607+ h1,
1608+ h2,
1609+ h3,
1610+ h4,
1611+ h5,
1612+ h6 {
1613+ color: #FFF;
1614+ }
1615+
1616+ a small {
1617+ color: white;
1618+ }
1619+ }
1620 diff --git a/sass/_layout.scss b/sass/_layout.scss
1621new file mode 100644
1622index 0000000..38fd882
1623--- /dev/null
1624+++ b/sass/_layout.scss
1625 @@ -0,0 +1,86 @@
1626+ @media print, screen and (max-width: 960px) {
1627+ div.wrapper {
1628+ width: auto;
1629+ margin: 0;
1630+ }
1631+
1632+ header,
1633+ section,
1634+ footer {
1635+ float: none;
1636+ position: static;
1637+ width: auto;
1638+ }
1639+
1640+ header {
1641+ padding-right: 320px;
1642+ }
1643+
1644+ section {
1645+ border: 1px solid #e5e5e5;
1646+ border-width: 1px 0;
1647+ padding: 20px 0;
1648+ margin: 0 0 20px;
1649+ }
1650+
1651+ header a small {
1652+ display: inline;
1653+ }
1654+
1655+ header ul {
1656+ position: absolute;
1657+ right: 50px;
1658+ top: 52px;
1659+ }
1660+ }
1661+
1662+ @media print, screen and (max-width: 720px) {
1663+ body {
1664+ word-wrap: break-word;
1665+ }
1666+
1667+ header {
1668+ padding: 0;
1669+ }
1670+
1671+ header ul,
1672+ header p.view {
1673+ position: static;
1674+ margin: 1rem auto;
1675+ }
1676+
1677+ pre,
1678+ code {
1679+ word-wrap: normal;
1680+ text-wrap: wrap;
1681+ }
1682+ }
1683+
1684+ @media print, screen and (max-width: 480px) {
1685+ body {
1686+ padding: 15px;
1687+ }
1688+
1689+ .downloads {
1690+ width: 100%;
1691+ margin: 1rem auto;
1692+ }
1693+
1694+ .downloads li,
1695+ .downloads li + li + li {
1696+ width: 33%;
1697+ }
1698+ }
1699+
1700+ @media print {
1701+ body {
1702+ padding: 0.4in;
1703+ font-size: 12pt;
1704+ color: #444;
1705+ }
1706+ }
1707+
1708+ ::selection {
1709+ background: rebeccapurple;
1710+ color: white;
1711+ }
1712 diff --git a/sass/_tables.scss b/sass/_tables.scss
1713new file mode 100644
1714index 0000000..96350d2
1715--- /dev/null
1716+++ b/sass/_tables.scss
1717 @@ -0,0 +1,22 @@
1718+ @use "variables";
1719+
1720+ table {
1721+ margin: 1rem auto;
1722+ border-collapse: collapse;
1723+ }
1724+
1725+ table thead th,
1726+ table tfoot th {
1727+ color: variables.$font-color;
1728+ background: rgba(0, 0, 0, 0.15);
1729+ }
1730+
1731+ table caption {
1732+ padding: 0.5em;
1733+ }
1734+
1735+ table th,
1736+ table td {
1737+ padding: 0.5em;
1738+ border: 1px solid variables.$border-color;
1739+ }
1740 diff --git a/sass/_variables.scss b/sass/_variables.scss
1741new file mode 100644
1742index 0000000..17151b8
1743--- /dev/null
1744+++ b/sass/_variables.scss
1745 @@ -0,0 +1,28 @@
1746+ // Variables
1747+ $font-color: #202020;
1748+ $bg-color: #fff;
1749+ $border-color: #e5e5e5;
1750+ $section-width: 500px;
1751+ $header-width: 270px;
1752+ $footer-width: 270px;
1753+ $wrapper-width: 860px;
1754+ $link-color: #267cb9;
1755+ $link-color-hover: #069;
1756+ $font-color-small: #777;
1757+ $font-color-code: #333;
1758+ $font-size-small: 11px;
1759+ $font-family: "Noto Sans", "Helvetica Neue", Helvetica, Arial, sans-serif;
1760+ $font-family-code:
1761+ Monaco,
1762+ Bitstream Vera Sans Mono,
1763+ Lucida Console,
1764+ Terminal,
1765+ Consolas,
1766+ Liberation Mono,
1767+ DejaVu Sans Mono,
1768+ Courier New,
1769+ monospace;
1770+
1771+ $media-max-width: 960px;
1772+ $media-max-width-smaller: 720px;
1773+ $media-max-width-even-smaller: 480px;
1774 diff --git a/sass/styles.scss b/sass/styles.scss
1775new file mode 100644
1776index 0000000..8fca72c
1777--- /dev/null
1778+++ b/sass/styles.scss
1779 @@ -0,0 +1,7 @@
1780+ @use "variables";
1781+ @use "base";
1782+ @use "classes";
1783+ @use "layout";
1784+ @use "tables";
1785+ @use "code";
1786+ @use "custom";
1787 diff --git a/static/android-chrome-192x192.png b/static/android-chrome-192x192.png
1788new file mode 100644
1789index 0000000..f6137eb
1790 Binary files /dev/null and b/static/android-chrome-192x192.png differ
1791 diff --git a/static/android-chrome-512x512.png b/static/android-chrome-512x512.png
1792new file mode 100644
1793index 0000000..c77c2af
1794 Binary files /dev/null and b/static/android-chrome-512x512.png differ
1795 diff --git a/static/apple-touch-icon.png b/static/apple-touch-icon.png
1796new file mode 100644
1797index 0000000..aaeaa8d
1798 Binary files /dev/null and b/static/apple-touch-icon.png differ
1799 diff --git a/static/favicon-16x16.png b/static/favicon-16x16.png
1800new file mode 100644
1801index 0000000..29dd09f
1802 Binary files /dev/null and b/static/favicon-16x16.png differ
1803 diff --git a/static/favicon-32x32.png b/static/favicon-32x32.png
1804new file mode 100644
1805index 0000000..811ba56
1806 Binary files /dev/null and b/static/favicon-32x32.png differ
1807 diff --git a/static/favicon.ico b/static/favicon.ico
1808new file mode 100644
1809index 0000000..de83549
1810 Binary files /dev/null and b/static/favicon.ico differ
1811 diff --git a/static/img/codeberg.svg b/static/img/codeberg.svg
1812new file mode 100644
1813index 0000000..068702a
1814--- /dev/null
1815+++ b/static/img/codeberg.svg
1816 @@ -0,0 +1,19 @@
1817+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" id="svg-badge" height="168" width="564" viewBox="0 0 564 168">
1818+ <defs>
1819+ <linearGradient xlink:href="#a" id="b" gradientUnits="userSpaceOnUse" x1="42519.285" y1="-7078.789" x2="42575.336" y2="-6966.931" />
1820+ <linearGradient id="a">
1821+ <stop offset="0" stop-color="#fff" stop-opacity="0" />
1822+ <stop offset=".495" stop-color="#71c2ff" />
1823+ <stop offset="1" stop-color="#39aaff" />
1824+ </linearGradient>
1825+ </defs>
1826+ <rect id="rect" ry="15.111" y="2" x="2" height="164" width="560" fill="none" stroke="#a6a6a6" stroke-width="4" stroke-linejoin="round" paint-order="fill stroke markers" />
1827+ <text style="line-height:0%;-inkscape-font-specification:'Inter';marker:none" x="203.16" y="-85.813" transform="matrix(1.0087 0 0 .99138 -19.194 133.848)" color="#000" font-weight="400" font-size="11.814" font-family="Inter, 'DejaVu Sans', sans-serif" letter-spacing="0" word-spacing="0" overflow="visible" fill="#2185d0" stroke-width=".985">
1828+ <tspan id="claim-svg" style="line-height:1;-inkscape-font-specification:'Inter'" x="203.16" y="-85.813" font-size="33.596">GET IT ON</tspan>
1829+ </text>
1830+ <path aria-label="Codeberg" d="M214.654 71.02c-8.453 0-15.047 2.79-19.803 8.369-4.803 5.577-7.215 12.943-7.215 22.096 0 7.034 1.764 13.158 5.281 18.372 4.862 7.275 12.274 10.912 22.2 10.912 6.907 0 12.922-2.516 18.049-7.548l-4.718-7.185c-4.57 2.85-8.512 4.275-11.844 4.275-5.069 0-9.112-1.91-12.12-5.729-2.847-3.638-4.271-8.004-4.271-13.097 0-5.76 1.211-10.428 3.613-14.005 2.773-4.183 6.694-6.274 11.747-6.274 3.65 0 7.684 1.574 12.11 4.728l4.734-7.546c-5.925-4.91-11.844-7.368-17.763-7.368zm167.7 0l-11.657 6.456v52.201h9.441l.925-4.091c2.843 3.455 6.844 5.183 12.03 5.183 5.238 0 9.638-1.548 13.224-4.64 4.56-4.061 6.85-10.033 6.85-17.914 0-6.122-1.547-11.033-4.634-14.732-3.523-4.184-8.38-6.275-14.612-6.275-2.226 0-4.266.418-6.105 1.274-2.348 1.09-4.176 2.607-5.462 4.546zm-62.745.183l-11.674 6.367v13.186c-2.895-2.304-6.471-3.455-10.722-3.455-4.93 0-9.16 1.606-12.667 4.82-4.692 4.304-7.056 10.427-7.056 18.37 0 5.638 1.466 10.247 4.362 13.824 3.454 4.304 8.486 6.456 15.085 6.456 2.343 0 4.622-.517 6.838-1.547 2.232-1.03 3.83-2.334 4.825-3.909l1.663 4.364h9.346zm-65.647 16.098c-6.057 0-10.956 1.878-14.718 5.636-4.139 4.123-6.206 9.52-6.206 16.188 0 5.88 1.855 10.854 5.553 14.916 4.134 4.486 9.272 6.73 15.371 6.73 5.255 0 9.84-1.759 13.783-5.275 4.75-4.183 7.125-9.641 7.125-16.371 0-5.758-1.599-10.638-4.808-14.641-3.826-4.79-9.192-7.183-16.1-7.183zm91.746 0c-5.988 0-10.914 1.94-14.819 5.82-4.314 4.245-6.477 9.609-6.477 16.096 0 6.61 1.923 11.792 5.744 15.552 4.017 4 9.596 6.002 16.758 6.002 6.647 0 12.205-1.94 16.641-5.822l-2.964-6.092c-4.368 2.426-8.56 3.637-12.572 3.637-2.842 0-5.302-.908-7.401-2.728-2.051-1.88-3.194-4.214-3.417-7.001h28.852v-4.183c0-6.003-1.6-10.885-4.809-14.643-3.693-4.427-8.884-6.638-15.536-6.638zm92.543 0c-5.978 0-10.909 1.94-14.803 5.82-4.326 4.245-6.472 9.609-6.472 16.096 0 6.61 1.907 11.792 5.733 15.552 4 4 9.585 6.002 16.753 6.002 6.658 0 12.205-1.94 16.652-5.822l-2.954-6.092c-4.389 2.426-8.587 3.637-12.588 3.637-2.837 0-5.308-.908-7.406-2.728-2.04-1.88-3.183-4.214-3.427-7.001h28.878v-4.183c0-6.003-1.605-10.885-4.814-14.643-3.709-4.427-8.879-6.638-15.552-6.638zm45.163 0c-1.838 0-3.672.485-5.462 1.455-1.727.908-2.901 2-3.512 3.272l-1.11-3.635h-9.9v41.286h11.477V100.03c.978-2.12 2.524-3.18 4.623-3.18 2.28 0 4.442 1.15 6.466 3.454l9.814-5.274c-2.71-5.154-6.844-7.73-12.396-7.73zm30.121 0c-4.883 0-9.107 1.606-12.683 4.82-4.691 4.304-7.024 10.427-7.024 18.37 0 5.577 1.435 10.186 4.336 13.824 3.4 4.304 8.188 6.456 14.351 6.456 5.064 0 8.974-2.032 11.759-6.094 0 9.094-3.342 13.642-10 13.642-4.697 0-8.921-1.213-12.683-3.638l-2.954 5.82c4.856 4.365 10.637 6.549 17.295 6.549 6.413 0 11.354-1.85 14.813-5.549 3.502-3.758 5.266-9.125 5.255-16.098v-37.01h-8.974l-.914 4.455c0-.604-.893-1.515-2.688-2.728-2.716-1.879-6.005-2.82-9.889-2.82zm-167.923 8.094c5.553 0 8.496 2.97 8.794 8.912h-17.869c.675-5.942 3.704-8.912 9.075-8.912zm92.554 0c5.547 0 8.474 2.97 8.788 8.912h-17.858c.674-5.942 3.703-8.912 9.07-8.912zm-184.204.089c5.989 0 8.98 4.547 8.98 13.64 0 8.732-2.991 13.097-8.98 13.097-5.982 0-8.98-4.365-8.98-13.096 0-9.094 2.998-13.641 8.98-13.641zm45.748 0c2.04 0 3.794.67 5.282 2.002 1.53 1.272 2.518 2.908 2.943 4.909v11.55c-.318 2.243-1.408 4.243-3.315 6.002-1.913 1.698-3.985 2.547-6.2 2.547-5.989 0-8.98-4.516-8.98-13.55 0-4.123.924-7.396 2.778-9.82 1.85-2.426 4.347-3.64 7.492-3.64zm216.312 0c1.982 0 3.762.7 5.377 2.093 1.669 1.335 2.625 2.94 2.875 4.818v10.46c0 2.607-.935 4.878-2.785 6.82-1.854 1.878-4.043 2.819-6.572 2.819-6.047 0-9.075-4.516-9.075-13.55 0-4.123.924-7.396 2.779-9.82 1.849-2.426 4.325-3.64 7.401-3.64zm-124.322.094c2.832 0 5.048 1.211 6.663 3.637 1.594 2.425 2.407 5.728 2.407 9.912 0 4.244-.866 7.58-2.598 10.006-1.727 2.424-4.07 3.637-7.019 3.637-2.295 0-4.31-.76-6.03-2.274-1.68-1.518-2.604-3.396-2.769-5.637v-9.916c0-2.606.893-4.818 2.673-6.637 1.854-1.817 4.075-2.728 6.673-2.728z" style="line-height:1.25;-inkscape-font-specification:'Tajawal Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal" font-weight="700" font-size="17.899" font-family="Tajawal" letter-spacing="-.285" word-spacing="0" fill="#2185d0" />
1831+ <g paint-order="stroke markers fill">
1832+ <path style="font-variation-settings:normal" d="M42519.285-7078.79a.76.568 0 00-.738.675l33.586 125.888a87.182 87.182 0 0039.381-33.763l-71.565-92.52a.76.568 0 00-.664-.28z" transform="matrix(.75692 0 0 .74393 -32088.397 5317.32)" opacity=".5" fill="url(#b)" />
1833+ <path d="M93.742 20.95A65.99 64.857 0 0028 85.807a65.99 64.857 0 0010.076 34.447l55.019-69.909a1.03.756 0 011.785 0l55.022 69.912a65.99 64.857 0 0010.078-34.45A65.99 64.857 0 0093.99 20.95a65.99 64.857 0 00-.248 0z" fill="#2185d0" />
1834+ </g>
1835+ </svg>
1836\ No newline at end of file
1837 diff --git a/static/img/logo.png b/static/img/logo.png
1838new file mode 100644
1839index 0000000..93e608e
1840 Binary files /dev/null and b/static/img/logo.png differ
1841 diff --git a/static/img/logo.svg b/static/img/logo.svg
1842new file mode 100644
1843index 0000000..cdc0267
1844--- /dev/null
1845+++ b/static/img/logo.svg
1846 @@ -0,0 +1 @@
1847+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200"><g transform="translate(0 -852.36)"><rect width="180" height="180" x="10" y="862.36" rx="8.546" ry="7.52" style="fill-opacity:.24215;fill:#e7eab7"/><path d="M159.21 52.277c0 12.686-11.082 22.97-24.752 22.97s-24.752-10.284-24.752-22.97 11.082-22.97 24.752-22.97 24.752 10.284 24.752 22.97z" style="fill-opacity:.89686;fill:#b3b3b3" transform="matrix(.6464 0 0 .69655 -38.912 967.95)"/><path d="M118.91 1020.4c.102-.453.25-.888.344-1.344a72.568 72.568 0 0 0 1.094-7.219c.248-2.444.375-4.927.375-7.437s-.127-4.993-.375-7.438-.61-4.851-1.094-7.218a72.281 72.281 0 0 0-1.813-6.97 72.276 72.276 0 0 0-2.437-6.687c-.92-2.175-1.943-4.283-3.063-6.343s-2.318-4.066-3.625-6-2.705-3.798-4.187-5.594a73.377 73.377 0 0 0-4.72-5.156c-1.644-1.645-3.36-3.237-5.155-4.72s-3.66-2.88-5.594-4.187c-1.934-1.306-3.94-2.505-6-3.625s-4.169-2.142-6.344-3.062a72.255 72.255 0 0 0-6.687-2.438 72.245 72.245 0 0 0-6.97-1.812 72.57 72.57 0 0 0-7.218-1.094c-2.445-.248-4.927-.375-7.437-.375s-4.993.127-7.438.375-4.852.61-7.219 1.094c-.456.093-.89.242-1.344.344v25.5c1.704-.552 3.345-1.23 5.125-1.594 1.595-.327 3.228-.583 4.875-.75s3.309-.25 5-.25 3.353.082 5 .25c1.648.167 3.28.423 4.875.75 3.19.652 6.257 1.604 9.188 2.844s5.737 2.77 8.344 4.53a49.125 49.125 0 0 1 7.25 5.97 49.138 49.138 0 0 1 5.969 7.25 49.135 49.135 0 0 1 4.53 8.343 48.502 48.502 0 0 1 2.845 9.188c.326 1.595.582 3.228.75 4.875s.25 3.308.25 5c0 1.691-.083 3.353-.25 5a49.126 49.126 0 0 1-.75 4.875c-.365 1.78-1.043 3.42-1.594 5.125h25.5z" style="fill-opacity:.89686;fill:#b3b3b3"/><path d="M48 874.36c-4.487 0-8.911.212-13.281.656-.92.094-1.805.294-2.719.407v29.562c1.894-.303 3.76-.68 5.688-.875 3.388-.345 6.833-.532 10.312-.532 3.478 0 6.924.187 10.312.532 3.388.344 6.72.86 10 1.53a99.75 99.75 0 0 1 18.906 5.844c3.014 1.275 5.956 2.699 8.812 4.25s5.632 3.252 8.313 5.063 5.261 3.727 7.75 5.781 4.876 4.252 7.156 6.531 4.478 4.668 6.531 7.157c2.054 2.488 3.97 5.069 5.781 7.75s3.511 5.456 5.063 8.312 2.975 5.798 4.25 8.813 2.393 6.094 3.375 9.25a99.975 99.975 0 0 1 2.469 9.656c.671 3.28 1.187 6.612 1.531 10 .344 3.388.531 6.834.531 10.313 0 3.479-.187 6.924-.531 10.312-.196 1.927-.572 3.794-.875 5.687h29.562c.112-.914.313-1.799.406-2.718.444-4.37.656-8.794.656-13.281 0-4.488-.212-8.911-.656-13.281-.444-4.37-1.134-8.675-2-12.906a129.23 129.23 0 0 0-3.187-12.47c-1.266-4.07-2.73-8.049-4.375-11.937a129.601 129.601 0 0 0-11.969-22.094c-2.336-3.458-4.85-6.79-7.5-10s-5.434-6.31-8.375-9.25-6.04-5.726-9.25-8.375-6.542-5.164-10-7.5a129.885 129.885 0 0 0-22.094-11.97 128.88 128.88 0 0 0-11.938-4.374 129.205 129.205 0 0 0-12.469-3.188c-4.231-.866-8.536-1.556-12.906-2-4.349-.48-8.773-.69-13.26-.69z" style="fill-opacity:.89686;fill:#b3b3b3"/></g></svg>
1848\ No newline at end of file
1849 diff --git a/static/img/minimal-favicon.png b/static/img/minimal-favicon.png
1850new file mode 100644
1851index 0000000..e7cdbde
1852 Binary files /dev/null and b/static/img/minimal-favicon.png differ
1853 diff --git a/static/img/minimal-minimal.png b/static/img/minimal-minimal.png
1854new file mode 100644
1855index 0000000..8f60a03
1856 Binary files /dev/null and b/static/img/minimal-minimal.png differ
1857 diff --git a/static/img/minimal-thumbnail.png b/static/img/minimal-thumbnail.png
1858new file mode 100644
1859index 0000000..e6c0478
1860 Binary files /dev/null and b/static/img/minimal-thumbnail.png differ
1861 diff --git a/static/img/sourcehut.svg b/static/img/sourcehut.svg
1862new file mode 100644
1863index 0000000..c7a12e9
1864--- /dev/null
1865+++ b/static/img/sourcehut.svg
1866 @@ -0,0 +1,77 @@
1867+ <?xml version="1.0" encoding="UTF-8" standalone="no"?>
1868+ <!-- Created with Inkscape (http://www.inkscape.org/) -->
1869+
1870+ <svg
1871+ width="300"
1872+ height="80"
1873+ viewBox="0 0 79.375001 21.166666"
1874+ version="1.1"
1875+ id="svg5"
1876+ inkscape:version="1.1 (c68e22c387, 2021-05-23)"
1877+ sodipodi:docname="sourcehut-black.svg"
1878+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
1879+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
1880+ xmlns="http://www.w3.org/2000/svg"
1881+ xmlns:svg="http://www.w3.org/2000/svg">
1882+ <sodipodi:namedview
1883+ id="namedview7"
1884+ pagecolor="#505050"
1885+ bordercolor="#ffffff"
1886+ borderopacity="1"
1887+ inkscape:pageshadow="0"
1888+ inkscape:pageopacity="0"
1889+ inkscape:pagecheckerboard="1"
1890+ inkscape:document-units="px"
1891+ showgrid="false"
1892+ units="px"
1893+ height="80px"
1894+ inkscape:zoom="3.0930415"
1895+ inkscape:cx="152.6006"
1896+ inkscape:cy="77.75518"
1897+ inkscape:window-width="1920"
1898+ inkscape:window-height="1059"
1899+ inkscape:window-x="0"
1900+ inkscape:window-y="0"
1901+ inkscape:window-maximized="1"
1902+ inkscape:current-layer="layer1" />
1903+ <defs
1904+ id="defs2">
1905+ <rect
1906+ x="72.43578"
1907+ y="4.989779"
1908+ width="233.3009"
1909+ height="93.363552"
1910+ id="rect22993" />
1911+ <rect
1912+ x="76.460692"
1913+ y="7.5029808"
1914+ width="210.43456"
1915+ height="46.880685"
1916+ id="rect5098" />
1917+ </defs>
1918+ <g
1919+ inkscape:label="Layer 1"
1920+ inkscape:groupmode="layer"
1921+ id="layer1">
1922+ <path
1923+ d="m 10.58314,1.3501934 c -5.1005652,0 -9.23314,4.1325746 -9.23314,9.2331386 0,5.100566 4.1325748,9.233141 9.23314,9.233141 5.100565,0 9.23314,-4.132575 9.23314,-9.233141 0,-5.100564 -4.132575,-9.2331386 -9.23314,-9.2331386 z m 0,16.6792216 c -4.1139596,0 -7.4460807,-3.332121 -7.4460807,-7.446083 0,-4.1139582 3.3321211,-7.4460793 7.4460807,-7.4460793 4.11396,0 7.446082,3.3321211 7.446082,7.4460793 0,4.113962 -3.332122,7.446083 -7.446082,7.446083 z"
1924+ id="path49"
1925+ style="stroke-width:0.0372303" />
1926+ <text
1927+ xml:space="preserve"
1928+ transform="scale(0.26458333)"
1929+ id="text22991"
1930+ style="fill:black;fill-opacity:1;line-height:1.25;stroke:none;font-family:sans-serif;font-style:normal;font-weight:normal;font-size:40px;white-space:pre;shape-inside:url(#rect22993)" />
1931+ <text
1932+ xml:space="preserve"
1933+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2889px;line-height:1.25;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.264583"
1934+ x="22.349714"
1935+ y="13.896598"
1936+ id="text29885"><tspan
1937+ sodipodi:role="line"
1938+ id="tspan29883"
1939+ style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:11.2889px;font-family:'DejaVu Sans';-inkscape-font-specification:'DejaVu Sans, Normal';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-variant-east-asian:normal;stroke-width:0.264583"
1940+ x="22.349714"
1941+ y="13.896598">sourcehut</tspan></text>
1942+ </g>
1943+ </svg>
1944 diff --git a/static/site.webmanifest b/static/site.webmanifest
1945new file mode 100644
1946index 0000000..06d341a
1947--- /dev/null
1948+++ b/static/site.webmanifest
1949 @@ -0,0 +1,19 @@
1950+ {
1951+ "name": "",
1952+ "short_name": "",
1953+ "icons": [
1954+ {
1955+ "src": "/android-chrome-192x192.png",
1956+ "sizes": "192x192",
1957+ "type": "image/png"
1958+ },
1959+ {
1960+ "src": "/android-chrome-512x512.png",
1961+ "sizes": "512x512",
1962+ "type": "image/png"
1963+ }
1964+ ],
1965+ "theme_color": "#ffffff",
1966+ "background_color": "#ffffff",
1967+ "display": "standalone"
1968+ }
1969 diff --git a/templates/base.html b/templates/base.html
1970new file mode 100644
1971index 0000000..18d1d59
1972--- /dev/null
1973+++ b/templates/base.html
1974 @@ -0,0 +1,55 @@
1975+ <!DOCTYPE html>
1976+ <html lang="en-US">
1977+ <head>
1978+ <meta charset="UTF-8" />
1979+ <meta http-equiv="X-UA-Compatible" content="IE=edge" />
1980+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1981+ <title>{% block title %}{{ config.title }}{% endblock title %}</title>
1982+ <meta
1983+ name="description"
1984+ itemprop="about"
1985+ content="{{config.description}}"
1986+ />
1987+ <link rel="stylesheet" href="{{ get_url(path='styles.css') | safe }}" />
1988+ </head>
1989+
1990+ <body>
1991+ <div class="wrapper">
1992+ <header>
1993+ <a href="{{ get_url(path='/') | safe }}">
1994+ <img
1995+ src="{{ get_url(path='img/logo.svg') | safe }}"
1996+ class="site-logo"
1997+ alt="Logo"
1998+ width="275px"
1999+ height="auto"
2000+ /></a>
2001+
2002+ <p>{{ config.description }}</p>
2003+
2004+
2005+ <a href="https://codeberg.org/ayllu/forge-feed">
2006+ <img
2007+ src="{{ get_url(path='img/codeberg.svg') | safe }}"
2008+ class="site-logo"
2009+ alt="Logo"
2010+ />
2011+ </a>
2012+
2013+ <a href="https://git.sr.ht/~kevinschoon/forge-feed">
2014+ <img
2015+ src="{{ get_url(path='img/sourcehut.svg') | safe }}"
2016+ class="site-logo sourcehut"
2017+ alt="Logo"
2018+ />
2019+ </a>
2020+ </header>
2021+
2022+ <section> {% block content %} {% endblock content %} </section>
2023+
2024+ <footer>
2025+ <p><small>.</small></p>
2026+ </footer>
2027+ </div>
2028+ </body>
2029+ </html>
2030 diff --git a/templates/index.html b/templates/index.html
2031new file mode 100644
2032index 0000000..3312f62
2033--- /dev/null
2034+++ b/templates/index.html
2035 @@ -0,0 +1,6 @@
2036+ {% extends "base.html" %}
2037+
2038+ {% block content %}
2039+ <h1>{{ section.title }}</h1>
2040+ {{ section.content | safe }}
2041+ {% endblock content %}
2042\ No newline at end of file
2043 diff --git a/templates/page.html b/templates/page.html
2044new file mode 100644
2045index 0000000..d911e83
2046--- /dev/null
2047+++ b/templates/page.html
2048 @@ -0,0 +1,6 @@
2049+ {% extends "base.html" %}
2050+
2051+ {% block content %}
2052+ <h1>{{ page.title }}</h1>
2053+ {{ page.content | safe }}
2054+ {% endblock content %}
2055\ No newline at end of file