mirror of
https://github.com/documize/community.git
synced 2025-07-23 07:09:43 +02:00
bulk data load methods for space and document views
This commit is contained in:
parent
bc72db711f
commit
318abef710
9 changed files with 315 additions and 29 deletions
16
README.md
16
README.md
|
@ -1,15 +1,19 @@
|
||||||
# Documize Community Edition
|
# Documize Community Edition
|
||||||
|
|
||||||
|
## The mission
|
||||||
|
|
||||||
|
To bring software development inspired features to the world of documenting -- refactoring, importing, testing, linting, metrics, PRs, versioning....
|
||||||
|
|
||||||
## What is it?
|
## What is it?
|
||||||
|
|
||||||
Documize is an intelligent document environment (IDE) for creating, securing and sharing documents -- everything yoyu need in one place.
|
Documize is an intelligent document environment (IDE) for creating, securing and sharing documents -- everything you need in one place.
|
||||||
|
|
||||||
## Why should I care?
|
## Why should I care?
|
||||||
|
|
||||||
Because maybe like us you are tired of:
|
Because maybe like us, you might be tired of:
|
||||||
|
|
||||||
* juggling WYSIWYG editors, wiki software and various document related solutions
|
* juggling WYSIWYG editors, wiki software and various document related solutions
|
||||||
* playing email tennis with document versions, contributions and feedback
|
* playing document related email tennis with contributions, versions and feedback
|
||||||
* sharing not-so-secure folders with external participants
|
* sharing not-so-secure folders with external participants
|
||||||
|
|
||||||
Sound familiar? Read on.
|
Sound familiar? Read on.
|
||||||
|
@ -18,7 +22,7 @@ Sound familiar? Read on.
|
||||||
|
|
||||||
Anyone who wants a single place for any kind of document.
|
Anyone who wants a single place for any kind of document.
|
||||||
|
|
||||||
Anyone who wants to loop in external participants without leaking information.
|
Anyone who wants to loop in external participants complete security.
|
||||||
|
|
||||||
Anyone who wishes documentation and knowledge capture worked like agile software development.
|
Anyone who wishes documentation and knowledge capture worked like agile software development.
|
||||||
|
|
||||||
|
@ -83,10 +87,6 @@ Documize is compatible with Auth0 identity as a service.
|
||||||
|
|
||||||
Open Source Identity and Access Management
|
Open Source Identity and Access Management
|
||||||
|
|
||||||
## The mission
|
|
||||||
|
|
||||||
To bring software development inspired features to the world of documenting -- refactoring, testing, linting, metrics, PRs, versioning....
|
|
||||||
|
|
||||||
## The legal bit at the end
|
## The legal bit at the end
|
||||||
|
|
||||||
<https://documize.com>
|
<https://documize.com>
|
||||||
|
|
|
@ -112,6 +112,7 @@ func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
cat, err := h.Store.Category.GetBySpace(ctx, spaceID)
|
cat, err := h.Store.Category.GetBySpace(ctx, spaceID)
|
||||||
if err != nil && err != sql.ErrNoRows {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
h.Runtime.Log.Error("get space categories visible to user failed", err)
|
||||||
response.WriteServerError(w, method, err)
|
response.WriteServerError(w, method, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -460,3 +461,66 @@ func (h *Handler) GetSpaceCategoryMembers(w http.ResponseWriter, r *http.Request
|
||||||
|
|
||||||
response.WriteJSON(w, cat)
|
response.WriteJSON(w, cat)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchSpaceData returns:
|
||||||
|
// 1. categories that user can see for given space
|
||||||
|
// 2. summary data for each category
|
||||||
|
// 3. category viewing membership records
|
||||||
|
func (h *Handler) FetchSpaceData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
method := "category.FetchSpaceData"
|
||||||
|
ctx := domain.GetRequestContext(r)
|
||||||
|
|
||||||
|
spaceID := request.Param(r, "spaceID")
|
||||||
|
if len(spaceID) == 0 {
|
||||||
|
response.WriteMissingDataError(w, method, "spaceID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ok := permission.HasPermission(ctx, *h.Store, spaceID, pm.SpaceManage, pm.SpaceOwner, pm.SpaceView)
|
||||||
|
if !ok {
|
||||||
|
response.WriteForbiddenError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch := category.FetchSpaceModel{}
|
||||||
|
|
||||||
|
// get space categories visible to user
|
||||||
|
cat, err := h.Store.Category.GetBySpace(ctx, spaceID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
h.Runtime.Log.Error("get space categories visible to user failed", err)
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(cat) == 0 {
|
||||||
|
cat = []category.Category{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// summary of space category usage
|
||||||
|
summary, err := h.Store.Category.GetSpaceCategorySummary(ctx, spaceID)
|
||||||
|
if err != nil {
|
||||||
|
h.Runtime.Log.Error("get space category summary failed", err)
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(summary) == 0 {
|
||||||
|
summary = []category.SummaryModel{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// get category membership records
|
||||||
|
member, err := h.Store.Category.GetSpaceCategoryMembership(ctx, spaceID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
h.Runtime.Log.Error("get document category membership for space", err)
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(member) == 0 {
|
||||||
|
member = []category.Member{}
|
||||||
|
}
|
||||||
|
|
||||||
|
fetch.Category = cat
|
||||||
|
fetch.Summary = summary
|
||||||
|
fetch.Membership = member
|
||||||
|
|
||||||
|
response.WriteJSON(w, fetch)
|
||||||
|
}
|
||||||
|
|
|
@ -29,7 +29,9 @@ import (
|
||||||
"github.com/documize/community/model/audit"
|
"github.com/documize/community/model/audit"
|
||||||
"github.com/documize/community/model/doc"
|
"github.com/documize/community/model/doc"
|
||||||
"github.com/documize/community/model/link"
|
"github.com/documize/community/model/link"
|
||||||
|
pm "github.com/documize/community/model/permission"
|
||||||
"github.com/documize/community/model/search"
|
"github.com/documize/community/model/search"
|
||||||
|
"github.com/documize/community/model/space"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Handler contains the runtime information such as logging and database.
|
// Handler contains the runtime information such as logging and database.
|
||||||
|
@ -371,3 +373,104 @@ func (h *Handler) SearchDocuments(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
response.WriteJSON(w, results)
|
response.WriteJSON(w, results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchDocumentData returns all document data in single API call.
|
||||||
|
func (h *Handler) FetchDocumentData(w http.ResponseWriter, r *http.Request) {
|
||||||
|
method := "document.FetchDocumentData"
|
||||||
|
ctx := domain.GetRequestContext(r)
|
||||||
|
|
||||||
|
id := request.Param(r, "documentID")
|
||||||
|
if len(id) == 0 {
|
||||||
|
response.WriteMissingDataError(w, method, "documentID")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// document
|
||||||
|
document, err := h.Store.Document.Get(ctx, id)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
response.WriteNotFoundError(w, method, id)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
h.Runtime.Log.Error(method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !permission.CanViewSpaceDocument(ctx, *h.Store, document.LabelID) {
|
||||||
|
response.WriteForbiddenError(w)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// permissions
|
||||||
|
perms, err := h.Store.Permission.GetUserSpacePermissions(ctx, document.LabelID)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(perms) == 0 {
|
||||||
|
perms = []pm.Permission{}
|
||||||
|
}
|
||||||
|
|
||||||
|
record := pm.DecodeUserPermissions(perms)
|
||||||
|
|
||||||
|
// links
|
||||||
|
l, err := h.Store.Link.GetDocumentOutboundLinks(ctx, id)
|
||||||
|
if len(l) == 0 {
|
||||||
|
l = []link.Link{}
|
||||||
|
}
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
h.Runtime.Log.Error(method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// spaces
|
||||||
|
sp, err := h.Store.Space.GetViewable(ctx)
|
||||||
|
if err != nil && err != sql.ErrNoRows {
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
h.Runtime.Log.Error(method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(sp) == 0 {
|
||||||
|
sp = []space.Space{}
|
||||||
|
}
|
||||||
|
|
||||||
|
data := documentData{}
|
||||||
|
data.Document = document
|
||||||
|
data.Permissions = record
|
||||||
|
data.Links = l
|
||||||
|
data.Spaces = sp
|
||||||
|
|
||||||
|
ctx.Transaction, err = h.Runtime.Db.Beginx()
|
||||||
|
if err != nil {
|
||||||
|
response.WriteServerError(w, method, err)
|
||||||
|
h.Runtime.Log.Error(method, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = h.Store.Activity.RecordUserActivity(ctx, activity.UserActivity{
|
||||||
|
LabelID: document.LabelID,
|
||||||
|
SourceID: document.RefID,
|
||||||
|
SourceType: activity.SourceTypeDocument,
|
||||||
|
ActivityType: activity.TypeRead})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
h.Runtime.Log.Error(method, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Store.Audit.Record(ctx, audit.EventTypeDocumentView)
|
||||||
|
|
||||||
|
ctx.Transaction.Commit()
|
||||||
|
|
||||||
|
response.WriteJSON(w, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// documentData represents all data associated for a single document.
|
||||||
|
// Used by FetchDocumentData() bulk data load call.
|
||||||
|
type documentData struct {
|
||||||
|
Document doc.Document `json:"document"`
|
||||||
|
Permissions pm.Record `json:"permissions"`
|
||||||
|
Spaces []space.Space `json:"folders"`
|
||||||
|
Links []link.Link `json:"link"`
|
||||||
|
}
|
||||||
|
|
|
@ -24,22 +24,31 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
|
||||||
this.set('documentId', this.paramsFor('document').document_id);
|
this.set('documentId', this.paramsFor('document').document_id);
|
||||||
|
|
||||||
return new Ember.RSVP.Promise((resolve) => {
|
return new Ember.RSVP.Promise((resolve) => {
|
||||||
this.get('documentService').getDocument(this.get('documentId')).then((document) => {
|
this.get('documentService').fetchDocumentData(this.get('documentId')).then((data) => {
|
||||||
this.set('document', document);
|
this.set('document', data.document);
|
||||||
|
this.set('folders', data.folders);
|
||||||
this.get('folderService').getAll().then((folders) => {
|
this.set('folder', data.folder);
|
||||||
this.set('folders', folders);
|
this.set('permissions', data.permissions);
|
||||||
|
this.set('links', data.links);
|
||||||
this.get('folderService').getFolder(this.get('folderId')).then((folder) => {
|
resolve();
|
||||||
this.set('folder', folder);
|
|
||||||
|
|
||||||
this.get('folderService').setCurrentFolder(folder).then(() => {
|
|
||||||
this.set('permissions', this.get('folderService').get('permissions'));
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// this.get('documentService').getDocument(this.get('documentId')).then((document) => {
|
||||||
|
// this.set('document', document);
|
||||||
|
|
||||||
|
// this.get('folderService').getAll().then((folders) => {
|
||||||
|
// this.set('folders', folders);
|
||||||
|
|
||||||
|
// this.get('folderService').getFolder(this.get('folderId')).then((folder) => {
|
||||||
|
// this.set('folder', folder);
|
||||||
|
|
||||||
|
// this.get('folderService').setCurrentFolder(folder).then(() => {
|
||||||
|
// this.set('permissions', this.get('folderService').get('permissions'));
|
||||||
|
// resolve();
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
// });
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -50,7 +59,7 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
|
||||||
document: this.get('document'),
|
document: this.get('document'),
|
||||||
page: this.get('pageId'),
|
page: this.get('pageId'),
|
||||||
permissions: this.get('permissions'),
|
permissions: this.get('permissions'),
|
||||||
links: this.get('linkService').getDocumentLinks(this.get('documentId')),
|
links: this.get('links'),
|
||||||
sections: this.get('sectionService').getAll()
|
sections: this.get('sectionService').getAll()
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -15,6 +15,18 @@ import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-rout
|
||||||
export default Ember.Route.extend(AuthenticatedRouteMixin, {
|
export default Ember.Route.extend(AuthenticatedRouteMixin, {
|
||||||
categoryService: Ember.inject.service('category'),
|
categoryService: Ember.inject.service('category'),
|
||||||
|
|
||||||
|
beforeModel() {
|
||||||
|
return new Ember.RSVP.Promise((resolve) => {
|
||||||
|
this.get('categoryService').fetchSpaceData(this.modelFor('folder').folder.get('id')).then((data) => {
|
||||||
|
this.set('categories', data.category);
|
||||||
|
this.set('categorySummary', data.summary);
|
||||||
|
this.set('categoryMembers', data.membership);
|
||||||
|
|
||||||
|
resolve(data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
model() {
|
model() {
|
||||||
this.get('browser').setTitle(this.modelFor('folder').folder.get('name'));
|
this.get('browser').setTitle(this.modelFor('folder').folder.get('name'));
|
||||||
|
|
||||||
|
@ -25,14 +37,25 @@ export default Ember.Route.extend(AuthenticatedRouteMixin, {
|
||||||
documents: this.modelFor('folder').documents,
|
documents: this.modelFor('folder').documents,
|
||||||
templates: this.modelFor('folder').templates,
|
templates: this.modelFor('folder').templates,
|
||||||
showStartDocument: false,
|
showStartDocument: false,
|
||||||
categories: this.get('categoryService').getUserVisible(this.modelFor('folder').folder.get('id')),
|
rootDocCount: 0,
|
||||||
categorySummary: this.get('categoryService').getSummary(this.modelFor('folder').folder.get('id')),
|
categories: this.get('categories'),
|
||||||
categoryMembers: this.get('categoryService').getSpaceCategoryMembership(this.modelFor('folder').folder.get('id')),
|
categorySummary: this.get('categorySummary'),
|
||||||
rootDocCount: 0
|
categoryMembers: this.get('categoryMembers'),
|
||||||
|
// categories: this.get('categoryService').getUserVisible(this.modelFor('folder').folder.get('id')),
|
||||||
|
// categorySummary: this.get('categoryService').getSummary(this.modelFor('folder').folder.get('id')),
|
||||||
|
// categoryMembers: this.get('categoryService').getSpaceCategoryMembership(this.modelFor('folder').folder.get('id')),
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
afterModel(model, transition) { // eslint-disable-line no-unused-vars
|
afterModel(model, transition) { // eslint-disable-line no-unused-vars
|
||||||
|
// model.folder = this.modelFor('folder').folder;
|
||||||
|
// model.permissions = this.modelFor('folder').permissions;
|
||||||
|
// model.folders = this.modelFor('folder').folders;
|
||||||
|
// model.documents = this.modelFor('folder').documents;
|
||||||
|
// model.templates = this.modelFor('folder').templates;
|
||||||
|
// model.showStartDocument = false;
|
||||||
|
// model.rootDocCount = 0;
|
||||||
|
|
||||||
let docs = model.documents;
|
let docs = model.documents;
|
||||||
let categoryMembers = model.categoryMembers;
|
let categoryMembers = model.categoryMembers;
|
||||||
let rootDocCount = 0;
|
let rootDocCount = 0;
|
||||||
|
|
|
@ -151,5 +151,34 @@ export default BaseService.extend({
|
||||||
}).then((response) => {
|
}).then((response) => {
|
||||||
return response;
|
return response;
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// fetchXXX represents UI specific bulk data loading designed to
|
||||||
|
// reduce network traffic and boost app performance.
|
||||||
|
// This method that returns:
|
||||||
|
// 1. getUserVisible()
|
||||||
|
// 2. getSummary()
|
||||||
|
// 3. getSpaceCategoryMembership()
|
||||||
|
fetchSpaceData(spaceId) {
|
||||||
|
return this.get('ajax').request(`fetch/category/space/${spaceId}`, {
|
||||||
|
method: 'GET'
|
||||||
|
}).then((response) => {
|
||||||
|
let data = {
|
||||||
|
category: [],
|
||||||
|
membership: [],
|
||||||
|
summary: []
|
||||||
|
};
|
||||||
|
|
||||||
|
let cats = response.category.map((obj) => {
|
||||||
|
let data = this.get('store').normalize('category', obj);
|
||||||
|
return this.get('store').push(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.category = cats;
|
||||||
|
data.membership = response.membership;
|
||||||
|
data.summary = response.summary;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -17,6 +17,7 @@ const {
|
||||||
|
|
||||||
export default Ember.Service.extend({
|
export default Ember.Service.extend({
|
||||||
sessionService: service('session'),
|
sessionService: service('session'),
|
||||||
|
folderService: service('folder'),
|
||||||
ajax: service(),
|
ajax: service(),
|
||||||
store: service(),
|
store: service(),
|
||||||
|
|
||||||
|
@ -308,6 +309,51 @@ export default Ember.Service.extend({
|
||||||
let data = this.get('store').normalize('page', response);
|
let data = this.get('store').normalize('page', response);
|
||||||
return this.get('store').push(data);
|
return this.get('store').push(data);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
//**************************************************
|
||||||
|
// Fetch bulk data
|
||||||
|
//**************************************************
|
||||||
|
|
||||||
|
// fetchXXX represents UI specific bulk data loading designed to
|
||||||
|
// reduce network traffic and boost app performance.
|
||||||
|
// This method that returns:
|
||||||
|
// 1. getUserVisible()
|
||||||
|
// 2. getSummary()
|
||||||
|
// 3. getSpaceCategoryMembership()
|
||||||
|
fetchDocumentData(documentId) {
|
||||||
|
return this.get('ajax').request(`fetch/document/${documentId}`, {
|
||||||
|
method: 'GET'
|
||||||
|
}).then((response) => {
|
||||||
|
let data = {
|
||||||
|
document: {},
|
||||||
|
permissions: {},
|
||||||
|
folders: [],
|
||||||
|
folder: {},
|
||||||
|
links: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
let doc = this.get('store').normalize('document', response.document);
|
||||||
|
doc = this.get('store').push(doc);
|
||||||
|
|
||||||
|
let perms = this.get('store').normalize('space-permission', response);
|
||||||
|
perms= this.get('store').push(perms);
|
||||||
|
this.get('folderService').set('permissions', perms);
|
||||||
|
|
||||||
|
let folders = response.folders.map((obj) => {
|
||||||
|
let data = this.get('store').normalize('folder', obj);
|
||||||
|
return this.get('store').push(data);
|
||||||
|
});
|
||||||
|
|
||||||
|
data.document = doc;
|
||||||
|
data.permissions = perms;
|
||||||
|
data.folders = folders;
|
||||||
|
data.folder = folders.findBy('id', doc.get('folderId'));
|
||||||
|
data.links = response.links;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -36,3 +36,11 @@ type SummaryModel struct {
|
||||||
CategoryID string `json:"categoryId"`
|
CategoryID string `json:"categoryId"`
|
||||||
Count int64 `json:"count"`
|
Count int64 `json:"count"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FetchSpaceModel represents categories, summary and membership in a single payload.
|
||||||
|
// Designed to speed up front-end app.
|
||||||
|
type FetchSpaceModel struct {
|
||||||
|
Category []Category `json:"category"`
|
||||||
|
Summary []SummaryModel `json:"summary"`
|
||||||
|
Membership []Member `json:"membership"`
|
||||||
|
}
|
||||||
|
|
|
@ -179,6 +179,10 @@ func RegisterEndpoints(rt *env.Runtime, s *domain.Store) {
|
||||||
Add(rt, RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, pin.UpdatePinSequence)
|
Add(rt, RoutePrefixPrivate, "pin/{userID}/sequence", []string{"POST", "OPTIONS"}, nil, pin.UpdatePinSequence)
|
||||||
Add(rt, RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, pin.DeleteUserPin)
|
Add(rt, RoutePrefixPrivate, "pin/{userID}/{pinID}", []string{"DELETE", "OPTIONS"}, nil, pin.DeleteUserPin)
|
||||||
|
|
||||||
|
// fetch methods exist to speed up UI rendering by returning data in bulk
|
||||||
|
Add(rt, RoutePrefixPrivate, "fetch/category/space/{spaceID}", []string{"GET", "OPTIONS"}, nil, category.FetchSpaceData)
|
||||||
|
Add(rt, RoutePrefixPrivate, "fetch/document/{documentID}", []string{"GET", "OPTIONS"}, nil, document.FetchDocumentData)
|
||||||
|
|
||||||
Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt)
|
Add(rt, RoutePrefixRoot, "robots.txt", []string{"GET", "OPTIONS"}, nil, meta.RobotsTxt)
|
||||||
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)
|
Add(rt, RoutePrefixRoot, "sitemap.xml", []string{"GET", "OPTIONS"}, nil, meta.Sitemap)
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue