aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmd/xesite/api.go76
-rw-r--r--cmd/xesite/main.go5
-rw-r--r--go.mod3
-rw-r--r--internal/jsonfeed/.gitignore17
-rw-r--r--internal/jsonfeed/LICENSE312
-rw-r--r--internal/jsonfeed/README.md6
-rw-r--r--internal/jsonfeed/jsonfeed.go77
-rw-r--r--internal/jsonfeed/jsonfeed_test.go42
-rw-r--r--internal/jsonfeed/testdata/feed.json21
-rw-r--r--lume/_config.ts2
-rw-r--r--lume/plugins/feed.ts279
-rw-r--r--pb/external/protofeed.proto8
-rw-r--r--pb/external/protofeed/protofeed.pb.go210
-rw-r--r--pb/openapi.json74
-rw-r--r--pb/xesite.pb.go14
-rw-r--r--pb/xesite.proto2
-rw-r--r--pb/xesite.twirp.go88
17 files changed, 1034 insertions, 202 deletions
diff --git a/cmd/xesite/api.go b/cmd/xesite/api.go
index 958396f..3a4eadb 100644
--- a/cmd/xesite/api.go
+++ b/cmd/xesite/api.go
@@ -2,6 +2,8 @@ package main
import (
"context"
+ "encoding/json"
+ "io/fs"
"os"
"os/exec"
"runtime"
@@ -10,8 +12,10 @@ import (
"github.com/twitchtv/twirp"
"google.golang.org/protobuf/types/known/emptypb"
"google.golang.org/protobuf/types/known/timestamppb"
+ "xeiaso.net/v4/internal/jsonfeed"
"xeiaso.net/v4/internal/lume"
"xeiaso.net/v4/pb"
+ "xeiaso.net/v4/pb/external/protofeed"
)
var denoVersion string
@@ -50,3 +54,75 @@ func (ms *MetaServer) Metadata(ctx context.Context, _ *emptypb.Empty) (*pb.Build
return result, nil
}
+
+type FeedServer struct {
+ fs *lume.FS
+}
+
+func (f *FeedServer) Get(ctx context.Context, _ *emptypb.Empty) (*protofeed.Feed, error) {
+ data, err := fs.ReadFile(f.fs, "blog.json")
+ if err != nil {
+ return nil, twirp.InternalErrorf("can't read blog.json: %w", err)
+ }
+
+ var feed jsonfeed.Feed
+
+ if err := json.Unmarshal(data, &feed); err != nil {
+ return nil, twirp.InternalErrorf("can't unmarshal blog.json: %w", err)
+ }
+
+ var result protofeed.Feed
+
+ result.Title = feed.Title
+ result.HomePageUrl = feed.HomePageURL
+ result.FeedUrl = feed.FeedURL
+ result.Description = feed.Description
+ result.UserComment = feed.UserComment
+ result.Icon = feed.Icon
+ result.Favicon = feed.Favicon
+ result.Expired = feed.Expired
+ result.Language = feed.Language
+ result.Items = make([]*protofeed.Item, len(feed.Items))
+ result.Authors = make([]*protofeed.Author, len(feed.Authors))
+
+ for i, item := range feed.Items {
+ var atts []*protofeed.Attachment
+ for _, att := range item.Attachments {
+ atts = append(atts, &protofeed.Attachment{
+ Url: att.URL,
+ MimeType: att.MIMEType,
+ Title: att.Title,
+ SizeInBytes: att.SizeInBytes,
+ DurationInSeconds: att.DurationInSeconds,
+ })
+ }
+
+ var authors []*protofeed.Author
+ for _, author := range item.Authors {
+ authors = append(authors, &protofeed.Author{
+ Name: author.Name,
+ Url: author.URL,
+ Avatar: author.Avatar,
+ })
+ }
+
+ result.Items[i] = &protofeed.Item{
+ Id: item.ID,
+ Url: item.URL,
+ ExternalUrl: item.ExternalURL,
+ Title: item.Title,
+ ContentHtml: item.ContentHTML,
+ ContentText: item.ContentText,
+ Summary: item.Summary,
+ Image: item.Image,
+ BannerImage: item.BannerImage,
+ DatePublished: timestamppb.New(item.DatePublished),
+ DateModified: timestamppb.New(item.DateModified),
+ Tags: item.Tags,
+ Authors: authors,
+ Attachments: atts,
+ }
+ }
+
+ return &result, nil
+}
diff --git a/cmd/xesite/main.go b/cmd/xesite/main.go
index c470855..d994431 100644
--- a/cmd/xesite/main.go
+++ b/cmd/xesite/main.go
@@ -11,9 +11,9 @@ import (
"path/filepath"
"github.com/donatj/hmacsig"
+ swaggerui "github.com/esceer/todo/swagger-ui"
"github.com/facebookgo/flagenv"
_ "github.com/joho/godotenv/autoload"
- "github.com/esceer/todo/swagger-ui"
"github.com/twitchtv/twirp"
"xeiaso.net/v4/internal"
"xeiaso.net/v4/internal/lume"
@@ -87,6 +87,9 @@ func main() {
ms := pb.NewMetaServer(&MetaServer{fs}, twirp.WithServerPathPrefix("/api"))
mux.Handle(ms.PathPrefix(), ms)
+ fsrv := pb.NewFeedServer(&FeedServer{fs}, twirp.WithServerPathPrefix("/api"))
+ mux.Handle(fsrv.PathPrefix(), fsrv)
+
mux.HandleFunc("/blog.atom", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, "/blog.rss", http.StatusMovedPermanently)
})
diff --git a/go.mod b/go.mod
index 50ce1c0..6907731 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
github.com/go-git/go-git/v5 v5.11.0
github.com/invopop/yaml v0.2.0
github.com/joho/godotenv v1.5.1
+ github.com/stretchr/testify v1.8.4
github.com/twitchtv/twirp v8.1.3+incompatible
golang.org/x/oauth2 v0.17.0
google.golang.org/protobuf v1.32.0
@@ -30,6 +31,7 @@ require (
github.com/ProtonMail/go-crypto v1.0.0 // indirect
github.com/cloudflare/circl v1.3.7 // indirect
github.com/cyphar/filepath-securejoin v0.2.4 // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c // indirect
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4 // indirect
@@ -46,6 +48,7 @@ require (
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
github.com/perimeterx/marshmallow v1.1.5 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/sergi/go-diff v1.3.1 // indirect
github.com/skeema/knownhosts v1.2.1 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
diff --git a/internal/jsonfeed/.gitignore b/internal/jsonfeed/.gitignore
new file mode 100644
index 0000000..f4d432a
--- /dev/null
+++ b/internal/jsonfeed/.gitignore
@@ -0,0 +1,17 @@
+# ---> Go
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
diff --git a/internal/jsonfeed/LICENSE b/internal/jsonfeed/LICENSE
new file mode 100644
index 0000000..09f2798
--- /dev/null
+++ b/internal/jsonfeed/LICENSE
@@ -0,0 +1,312 @@
+Mozilla Public License Version 2.0
+
+ 1. Definitions
+
+1.1. "Contributor" means each individual or legal entity that creates, contributes
+to the creation of, or owns Covered Software.
+
+1.2. "Contributor Version" means the combination of the Contributions of others
+(if any) used by a Contributor and that particular Contributor's Contribution.
+
+ 1.3. "Contribution" means Covered Software of a particular Contributor.
+
+1.4. "Covered Software" means Source Code Form to which the initial Contributor
+has attached the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case including portions
+thereof.
+
+ 1.5. "Incompatible With Secondary Licenses" means
+
+(a) that the initial Contributor has attached the notice described in Exhibit
+B to the Covered Software; or
+
+(b) that the Covered Software was made available under the terms of version
+1.1 or earlier of the License, but not also under the terms of a Secondary
+License.
+
+1.6. "Executable Form" means any form of the work other than Source Code Form.
+
+1.7. "Larger Work" means a work that combines Covered Software with other
+material, in a separate file or files, that is not Covered Software.
+
+ 1.8. "License" means this document.
+
+1.9. "Licensable" means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and all of the
+rights conveyed by this License.
+
+ 1.10. "Modifications" means any of the following:
+
+(a) any file in Source Code Form that results from an addition to, deletion
+from, or modification of the contents of Covered Software; or
+
+(b) any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor means any patent claim(s), including
+without limitation, method, process, and apparatus claims, in any patent Licensable
+by such Contributor that would be infringed, but for the grant of the License,
+by the making, using, selling, offering for sale, having made, import, or
+transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License" means either the GNU General Public License, Version
+2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form" means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your") means an individual or a legal entity exercising rights
+under this License. For legal entities, "You" includes any entity that controls,
+is controlled by, or is under common control with You. For purposes of this
+definition, "control" means (a) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or otherwise,
+or (b) ownership of more than fifty percent (50%) of the outstanding shares
+or beneficial ownership of such entity.
+
+ 2. License Grants and Conditions
+
+ 2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive
+license:
+
+(a) under intellectual property rights (other than patent or trademark) Licensable
+by such Contributor to use, reproduce, make available, modify, display, perform,
+distribute, and otherwise exploit its Contributions, either on an unmodified
+basis, with Modifications, or as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer for
+sale, have made, import, and otherwise transfer either its Contributions or
+its Contributor Version.
+
+ 2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution become
+effective for each Contribution on the date the Contributor first distributes
+such Contribution.
+
+ 2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under this
+License. No additional rights or licenses will be implied from the distribution
+or licensing of Covered Software under this License. Notwithstanding Section
+2.1(b) above, no patent license is granted by a Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software; or
+
+(b) for infringements caused by: (i) Your and any other third party's modifications
+of Covered Software, or (ii) the combination of its Contributions with other
+software (except as part of its Contributor Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of its
+Contributions.
+
+This License does not grant any rights in the trademarks, service marks, or
+logos of any Contributor (except as may be necessary to comply with the notice
+requirements in Section 3.4).
+
+ 2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to distribute
+the Covered Software under a subsequent version of this License (see Section
+10.2) or under the terms of a Secondary License (if permitted under the terms
+of Section 3.3).
+
+ 2.5. Representation
+
+Each Contributor represents that the Contributor believes its Contributions
+are its original creation(s) or it has sufficient rights to grant the rights
+to its Contributions conveyed by this License.
+
+ 2.6. Fair Use
+
+This License is not intended to limit any rights You have under applicable
+copyright doctrines of fair use, fair dealing, or other equivalents.
+
+ 2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+Section 2.1.
+
+ 3. Responsibilities
+
+ 3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any Modifications
+that You create or to which You contribute, must be under the terms of this
+License. You must inform recipients that the Source Code Form of the Covered
+Software is governed by the terms of this License, and how they can obtain
+a copy of this License. You may not attempt to alter or restrict the recipients'
+rights in the Source Code Form.
+
+ 3.2. Distribution of Executable Form
+
+ If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code Form,
+as described in Section 3.1, and You must inform recipients of the Executable
+Form how they can obtain a copy of such Source Code Form by reasonable means
+in a timely manner, at a charge no more than the cost of distribution to the
+recipient; and
+
+(b) You may distribute such Executable Form under the terms of this License,
+or sublicense it under different terms, provided that the license for the
+Executable Form does not attempt to limit or alter the recipients' rights
+in the Source Code Form under this License.
+
+ 3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice, provided
+that You also comply with the requirements of this License for the Covered
+Software. If the Larger Work is a combination of Covered Software with a work
+governed by one or more Secondary Licenses, and the Covered Software is not
+Incompatible With Secondary Licenses, this License permits You to additionally
+distribute such Covered Software under the terms of such Secondary License(s),
+so that the recipient of the Larger Work may, at their option, further distribute
+the Covered Software under the terms of either this License or such Secondary
+License(s).
+
+ 3.4. Notices
+
+You may not remove or alter the substance of any license notices (including
+copyright notices, patent notices, disclaimers of warranty, or limitations
+of liability) contained within the Source Code Form of the Covered Software,
+except that You may alter any license notices to the extent required to remedy
+known factual inaccuracies.
+
+ 3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support, indemnity
+or liability obligations to one or more recipients of Covered Software. However,
+You may do so only on Your own behalf, and not on behalf of any Contributor.
+You must make it absolutely clear that any such warranty, support, indemnity,
+or liability obligation is offered by You alone, and You hereby agree to indemnify
+every Contributor for any liability incurred by such Contributor as a result
+of warranty, support, indemnity or liability terms You offer. You may include
+additional disclaimers of warranty and limitations of liability specific to
+any jurisdiction.
+
+ 4. Inability to Comply Due to Statute or Regulation
+
+If it is impossible for You to comply with any of the terms of this License
+with respect to some or all of the Covered Software due to statute, judicial
+order, or regulation then You must: (a) comply with the terms of this License
+to the maximum extent possible; and (b) describe the limitations and the code
+they affect. Such description must be placed in a text file included with
+all distributions of the Covered Software under this License. Except to the
+extent prohibited by statute or regulation, such description must be sufficiently
+detailed for a recipient of ordinary skill to be able to understand it.
+
+ 5. Termination
+
+5.1. The rights granted under this License will terminate automatically if
+You fail to comply with any of its terms. However, if You become compliant,
+then the rights granted under this License from a particular Contributor are
+reinstated (a) provisionally, unless and until such Contributor explicitly
+and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor
+fails to notify You of the non-compliance by some reasonable means prior to
+60 days after You have come back into compliance. Moreover, Your grants from
+a particular Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the first
+time You have received notice of non-compliance with this License from such
+Contributor, and You become compliant prior to 30 days after Your receipt
+of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent infringement
+claim (excluding declaratory judgment actions, counter-claims, and cross-claims)
+alleging that a Contributor Version directly or indirectly infringes any patent,
+then the rights granted to You by any and all Contributors for the Covered
+Software under Section 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end
+user license agreements (excluding distributors and resellers) which have
+been validly granted by You or Your distributors under this License prior
+to termination shall survive termination.
+
+ 6. Disclaimer of Warranty
+
+Covered Software is provided under this License on an "as is" basis, without
+warranty of any kind, either expressed, implied, or statutory, including,
+without limitation, warranties that the Covered Software is free of defects,
+merchantable, fit for a particular purpose or non-infringing. The entire risk
+as to the quality and performance of the Covered Software is with You. Should
+any Covered Software prove defective in any respect, You (not any Contributor)
+assume the cost of any necessary servicing, repair, or correction. This disclaimer
+of warranty constitutes an essential part of this License. No use of any Covered
+Software is authorized under this License except under this disclaimer.
+
+ 7. Limitation of Liability
+
+Under no circumstances and under no legal theory, whether tort (including
+negligence), contract, or otherwise, shall any Contributor, or anyone who
+distributes Covered Software as permitted above, be liable to You for any
+direct, indirect, special, incidental, or consequential damages of any character
+including, without limitation, damages for lost profits, loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all other commercial
+damages or losses, even if such party shall have been informed of the possibility
+of such damages. This limitation of liability shall not apply to liability
+for death or personal injury resulting from such party's negligence to the
+extent applicable law prohibits such limitation. Some jurisdictions do not
+allow the exclusion or limitation of incidental or consequential damages,
+so this exclusion and limitation may not apply to You.
+
+ 8. Litigation
+
+Any litigation relating to this License may be brought only in the courts
+of a jurisdiction where the defendant maintains its principal place of business
+and such litigation shall be governed by laws of that jurisdiction, without
+reference to its conflict-of-law provisions. Nothing in this Section shall
+prevent a party's ability to bring cross-claims or counter-claims.
+
+ 9. Miscellaneous
+
+This License represents the complete agreement concerning the subject matter
+hereof. If any provision of this License is held to be unenforceable, such
+provision shall be reformed only to the extent necessary to make it enforceable.
+Any law or regulation which provides that the language of a contract shall
+be construed against the drafter shall not be used to construe this License
+against a Contributor.
+
+ 10. Versions of the License
+
+ 10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section 10.3,
+no one other than the license steward has the right to modify or publish new
+versions of this License. Each version will be given a distinguishing version
+number.
+
+ 10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version of
+the License under which You originally received the Covered Software, or under
+the terms of any subsequent version published by the license steward.
+
+ 10.3. Modified Versions
+
+If you create software not governed by this License, and you want to create
+a new license for such software, you may create and use a modified version
+of this License if you rename the license and remove any references to the
+name of the license steward (except to note that such modified license differs
+from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With Secondary
+Licenses under the terms of this version of the License, the notice described
+in Exhibit B of this License must be attached. Exhibit A - Source Code Form
+License Notice
+
+This Source Code Form is subject to the terms of the Mozilla Public License,
+v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
+one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+This Source Code Form is "Incompatible With Secondary Licenses", as defined
+by the Mozilla Public License, v. 2.0.
diff --git a/internal/jsonfeed/README.md b/internal/jsonfeed/README.md
new file mode 100644
index 0000000..e3dbd61
--- /dev/null
+++ b/internal/jsonfeed/README.md
@@ -0,0 +1,6 @@
+# jsonfeed
+
+[![Go Report Card](https://goreportcard.com/badge/christine.website/jsonfeed)](https://goreportcard.com/report/christine.website/jsonfeed)
+[![Build Status](https://drone.tulpa.dev/api/badges/Xe/jsonfeed/status.svg)](https://drone.tulpa.dev/Xe/jsonfeed)
+
+JSONFeed support for Go
diff --git a/internal/jsonfeed/jsonfeed.go b/internal/jsonfeed/jsonfeed.go
new file mode 100644
index 0000000..684bd86
--- /dev/null
+++ b/internal/jsonfeed/jsonfeed.go
@@ -0,0 +1,77 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/
+
+package jsonfeed
+
+import (
+ "encoding/json"
+ "io"
+ "time"
+)
+
+const CurrentVersion = "https://jsonfeed.org/version/1"
+
+type Item struct {
+ ID string `json:"id"`
+ URL string `json:"url"`
+ ExternalURL string `json:"external_url"`
+ Title string `json:"title"`
+ ContentHTML string `json:"content_html"`
+ ContentText string `json:"content_text"`
+ Summary string `json:"summary"`
+ Image string `json:"image"`
+ BannerImage string `json:"banner_image"`
+ DatePublished time.Time `json:"date_published"`
+ DateModified time.Time `json:"date_modified"`
+ Author Author `json:"author"`
+ Authors []Author `json:"authors"`
+ Tags []string `json:"tags"`
+ Attachments []Attachment `json:"attachments"`
+}
+
+type Author struct {
+ Name string `json:"name"`
+ URL string `json:"url"`
+ Avatar string `json:"avatar"`
+}
+
+type Hub struct {
+ Type string `json:"type"`
+ URL string `json:"url"`
+}
+
+type Attachment struct {
+ URL string `json:"url"`
+ MIMEType string `json:"mime_type"`
+ Title string `json:"title"`
+ SizeInBytes int32 `json:"size_in_bytes"`
+ DurationInSeconds int32 `json:"duration_in_seconds"`
+}
+
+type Feed struct {
+ Version string `json:"version"`
+ Title string `json:"title"`
+ HomePageURL string `json:"home_page_url"`
+ FeedURL string `json:"feed_url"`
+ Description string `json:"description"`
+ UserComment string `json:"user_comment"`
+ NextURL string `json:"next_url"`
+ Icon string `json:"icon"`
+ Favicon string `json:"favicon"`
+ Author Author `json:"author"`
+ Authors []Author `json:"authors"`
+ Language string `json:"language"`
+ Expired bool `json:"expired"`
+ Hubs []Hub `json:"hubs"`
+ Items []Item `json:"items"`
+}
+
+func Parse(r io.Reader) (Feed, error) {
+ var feed Feed
+ decoder := json.NewDecoder(r)
+ if err := decoder.Decode(&feed); err != nil {
+ return Feed{}, err
+ }
+ return feed, nil
+}
diff --git a/internal/jsonfeed/jsonfeed_test.go b/internal/jsonfeed/jsonfeed_test.go
new file mode 100644
index 0000000..d345f38
--- /dev/null
+++ b/internal/jsonfeed/jsonfeed_test.go
@@ -0,0 +1,42 @@
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at http://mozilla.org/MPL/2.0/
+
+package jsonfeed
+
+import (
+ "os"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestParseSimple(t *testing.T) {
+ r, err := os.Open("testdata/feed.json")
+ assert.NoError(t, err, "Could not open testdata/feed.json")
+
+ feed, err := Parse(r)
+ assert.NoError(t, err, "Could not parse testdata/feed.json")
+
+ assert.Equal(t, "https://jsonfeed.org/version/1", feed.Version)
+ assert.Equal(t, "JSON Feed", feed.Title)
+ assert.Equal(t, "JSON Feed is a ...", feed.Description)
+ assert.Equal(t, "https://jsonfeed.org/", feed.HomePageURL)
+ assert.Equal(t, "https://jsonfeed.org/feed.json", feed.FeedURL)
+ assert.Equal(t, "This feed allows ...", feed.UserComment)
+ assert.Equal(t, "https://jsonfeed.org/graphics/icon.png", feed.Favicon)
+ assert.Equal(t, "Brent Simmons and Manton Reece", feed.Author.Name)
+
+ assert.Equal(t, 1, len(feed.Items))
+
+ assert.Equal(t, "https://jsonfeed.org/2017/05/17/announcing_json_feed", feed.Items[0].ID)
+ assert.Equal(t, "https://jsonfeed.org/2017/05/17/announcing_json_feed", feed.Items[0].URL)
+ assert.Equal(t, "Announcing JSON Feed", feed.Items[0].Title)
+ assert.Equal(t, "<p>We ...", feed.Items[0].ContentHTML)
+
+ datePublished, err := time.Parse("2006-01-02T15:04:05-07:00", "2017-05-17T08:02:12-07:00")
+ assert.NoError(t, err, "Could not parse timestamp")
+
+ assert.Equal(t, datePublished, feed.Items[0].DatePublished)
+}
diff --git a/internal/jsonfeed/testdata/feed.json b/internal/jsonfeed/testdata/feed.json
new file mode 100644
index 0000000..ad4bbe1
--- /dev/null
+++ b/internal/jsonfeed/testdata/feed.json
@@ -0,0 +1,21 @@
+{
+ "version": "https://jsonfeed.org/version/1",
+ "title": "JSON Feed",
+ "description": "JSON Feed is a ...",
+ "home_page_url": "https://jsonfeed.org/",
+ "feed_url": "https://jsonfeed.org/feed.json",
+ "user_comment": "This feed allows ...",
+ "favicon": "https://jsonfeed.org/graphics/icon.png",
+ "author": {
+ "name": "Brent Simmons and Manton Reece"
+ },
+ "items": [
+ {
+ "id": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
+ "url": "https://jsonfeed.org/2017/05/17/announcing_json_feed",
+ "title": "Announcing JSON Feed",
+ "content_html": "<p>We ...",
+ "date_published": "2017-05-17T08:02:12-07:00"
+ }
+ ]
+}
diff --git a/lume/_config.ts b/lume/_config.ts
index 87e5bc8..4ccd4da 100644
--- a/lume/_config.ts
+++ b/lume/_config.ts
@@ -4,7 +4,6 @@ import attributes from "lume/plugins/attributes.ts";
import nunjucks from "lume/plugins/nunjucks.ts";
import date from "lume/plugins/date.ts";
import esbuild from "lume/plugins/esbuild.ts";
-import feed from "lume/plugins/feed.ts";
import mdx from "lume/plugins/mdx.ts";
import tailwindcss from "lume/plugins/tailwindcss.ts";
import postcss from "lume/plugins/postcss.ts";
@@ -12,6 +11,7 @@ import sitemap from "lume/plugins/sitemap.ts";
import readInfo from "lume/plugins/reading_info.ts";
import annotateYear from "./plugins/annotate_year.ts";
+import feed from "./plugins/feed.ts";
//import pagefind from "lume/plugins/pagefind.ts";
//import _ from "npm:@pagefind/linux-x64";
diff --git a/lume/plugins/feed.ts b/lume/plugins/feed.ts
new file mode 100644
index 0000000..8214b41
--- /dev/null
+++ b/lume/plugins/feed.ts
@@ -0,0 +1,279 @@
+import { getExtension } from "lume/core/utils/path.ts";
+import { merge } from "lume/core/utils/object.ts";
+import { getCurrentVersion } from "lume/core/utils/lume_version.ts";
+import { getDataValue } from "lume/core/utils/data_values.ts";
+import { $XML, stringify } from "lume/deps/xml.ts";
+import { Page } from "lume/core/file.ts";
+
+import type Site from "lume/core/site.ts";
+import type { Data } from "lume/core/file.ts";
+
+export interface Options {
+ /** The output filenames */
+ output?: string | string[];
+
+ /** The query to search the pages */
+ query?: string;
+
+ /** The sort order */
+ sort?: string;
+
+ /** The maximum number of items */
+ limit?: number;
+
+ /** The feed info */
+ info?: FeedInfoOptions;
+
+ /** The feed items configuration */
+ items?: FeedItemOptions;
+}
+
+export interface FeedInfoOptions {
+ /** The feed title */
+ title?: string;
+
+ /** The feed subtitle */
+ subtitle?: string;
+
+ /**
+ * The feed published date
+ * @default `new Date()`
+ */
+ published?: Date;
+
+ /** The feed description */
+ description?: string;
+
+ /** The feed language */
+ lang?: string;
+
+ /** The feed generator. Set `true` to generate automatically */
+ generator?: string | boolean;
+}
+
+export interface FeedItemOptions {
+ /** The item title */
+ title?: string | ((data: Data) => string | undefined);
+
+ /** The item description */
+ description?: string | ((data: Data) => string | undefined);
+
+ /** The item published date */
+ published?: string | ((data: Data) => Date | undefined);
+
+ /** The item updated date */
+ updated?: string | ((data: Data) => Date | undefined);
+
+ /** The item content */
+ content?: string | ((data: Data) => string | undefined);
+
+ /** The item language */
+ lang?: string | ((data: Data) => string | undefined);
+}
+
+export const defaults: Options = {
+ /** The output filenames */
+ output: "/feed.rss",
+
+ /** The query to search the pages */
+ query: "",
+
+ /** The sort order */
+ sort: "date=desc",
+
+ /** The maximum number of items */
+ limit: 10,
+
+ /** The feed info */
+ info: {
+ title: "My RSS Feed",
+ published: new Date(),
+ description: "",
+ lang: "en",
+ generator: true,
+ },
+ items: {
+ title: "=title",
+ description: "=description",
+ published: "=date",
+ content: "=children",
+ lang: "=lang",
+ },
+};
+
+export interface FeedData {
+ title: string;
+ url: string;
+ description: string;
+ published: Date;
+ lang: string;
+ generator?: string;
+ items: FeedItem[];
+}
+
+export interface FeedItem {
+ title: string;
+ url: string;
+ description: string;
+ published: Date;
+ updated?: Date;
+ content: string;
+ lang: string;
+}
+
+const defaultGenerator = `Lume ${getCurrentVersion()}`;
+
+export default function (userOptions?: Options) {
+ const options = merge(defaults, userOptions);
+
+ return (site: Site) => {
+ site.addEventListener("beforeSave", () => {
+ const output = Array.isArray(options.output)
+ ? options.output
+ : [options.output];
+
+ const pages = site.search.pages(
+ options.query,
+ options.sort,
+ options.limit,
+ ) as Data[];
+
+ const { info, items } = options;
+ const rootData = site.source.data.get("/") || {};
+
+ const feed: FeedData = {
+ title: getDataValue(rootData, info.title),
+ description: getDataValue(rootData, info.description),
+ published: getDataValue(rootData, info.published),
+ lang: getDataValue(rootData, info.lang),
+ url: site.url("", true),
+ generator: info.generator === true
+ ? defaultGenerator
+ : info.generator || undefined,
+ items: pages.map((data): FeedItem => {
+ const content = getDataValue(data, items.content)?.toString();
+ const pageUrl = site.url(data.url, true);
+ const fixedContent = fixUrls(new URL(pageUrl), content || "");
+
+ return {
+ title: getDataValue(data, items.title),
+ url: site.url(data.url, true),
+ description: getDataValue(data, items.description),
+ published: getDataValue(data, items.published),
+ updated: getDataValue(data, items.updated),
+ content: fixedContent,
+ lang: getDataValue(data, items.lang),
+ };
+ }),
+ };
+
+ for (const filename of output) {
+ const format = getExtension(filename).slice(1);
+ const file = site.url(filename, true);
+
+ switch (format) {
+ case "rss":
+ case "feed":
+ case "xml":
+ site.pages.push(
+ Page.create({ url: filename, content: generateRss(feed, file) }),
+ );
+ break;
+
+ case "json":
+ site.pages.push(
+ Page.create({ url: filename, content: generateJson(feed, file) }),
+ );
+