Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c7a0d51
(feat): generate sitemaps on demand and store in collection
dancastellon Jul 9, 2018
f671b6c
(style): correct linting issues
dancastellon Jul 10, 2018
59226ad
(feat): Routes for auto-generated sitemap files
dancastellon Jul 10, 2018
db2d035
(feat): Add compound index for Sitemaps queries
dancastellon Jul 10, 2018
56de04e
(refactor): Cleanup generateSitemaps function
dancastellon Jul 11, 2018
b423901
(style): corrent linting issues
dancastellon Jul 11, 2018
a7a11c2
(feat): sitemap refresh period setting + translations
dancastellon Jul 11, 2018
84fae7f
(refactor): rename sitemap route handler to middleware
dancastellon Jul 11, 2018
4ea11c8
(feat): recurring job that regenerates sitemaps
dancastellon Jul 11, 2018
6612677
(style): corrent linting issues
dancastellon Jul 11, 2018
720b3bd
(perf): limit default # URLs per sitemap to 1k + use job-collection f…
dancastellon Jul 12, 2018
28485ce
(test): tests for generateSitemaps function
dancastellon Jul 12, 2018
0e56dbf
(test): additional tests for generateSitemaps
dancastellon Jul 13, 2018
a7b7d4a
(test): app test for getSitemapXML
dancastellon Jul 13, 2018
316a6cd
Merge branch 'release-1.14.0' of https://github.com/reactioncommerce/…
dancastellon Jul 18, 2018
6f22ff5
(style): correct import order
dancastellon Jul 18, 2018
a02efcf
(fix): Make settings.cart optional in CorePackageConfig schema
dancastellon Jul 18, 2018
d5c641e
(style): use hyphenated filenames
dancastellon Jul 18, 2018
3ad5886
(fix): sitemap generation button shouldn't be main form submit
dancastellon Jul 18, 2018
d2449e0
(style): No need for default translation in alerts
dancastellon Jul 18, 2018
36cc784
(style) no periods in translations
dancastellon Jul 18, 2018
b85421f
(style) methods/index.js should only import & export
dancastellon Jul 18, 2018
389bb5a
(style) correct linting issues
dancastellon Jul 18, 2018
01602a2
(fix): uncapitalize sitemaps.js filename
dancastellon Jul 19, 2018
c6461a7
(feat): only regen tag & product sitemaps when new/updated record exi…
dancastellon Jul 19, 2018
3f1fedc
Merge branch 'release-1.14.0' of https://github.com/reactioncommerce/…
dancastellon Jul 23, 2018
44a5992
Merge branch 'release-1.14.0' of https://github.com/reactioncommerce/…
dancastellon Jul 24, 2018
d848c6c
(refactor): max 2 params for rebuildPaginatedSitemaps function
dancastellon Jul 24, 2018
8d39151
(feat): publication for last sitemap generation date
dancastellon Jul 24, 2018
734aca8
(feat): better description for sitemap generation alert
dancastellon Jul 24, 2018
a5e2c7b
(feat): view sitemap link + last generation date in Shop Options
dancastellon Jul 24, 2018
0fba24a
(style): correct eslint issues
dancastellon Jul 24, 2018
1f2164a
Merge branch 'release-1.14.0' of https://github.com/reactioncommerce/…
dancastellon Jul 24, 2018
7b42479
(style): correct import order
dancastellon Jul 24, 2018
6c88323
Merge branch 'release-1.14.0' of https://github.com/reactioncommerce/…
dancastellon Jul 25, 2018
7b55ac4
(fix) update sitemap generation translations
dancastellon Jul 25, 2018
20adb5c
(feat): use notifications UI when manually refreshing sitemap
dancastellon Jul 25, 2018
db0f013
(style): correct linting issues
dancastellon Jul 25, 2018
743aea3
(fix): change sitemap refresh notification text
dancastellon Jul 25, 2018
a9595b4
Merge branch 'release-1.14.0' of https://github.com/reactioncommerce/…
dancastellon Jul 27, 2018
7abfef3
(fix): provide default values for toast alerts
dancastellon Jul 27, 2018
8d5fe24
(feat): sitemap refresh notification brings admin to sitemap.xml
dancastellon Jul 27, 2018
1c51a91
(feat): select box for sitemap refresh period + use Reaction componen…
dancastellon Jul 27, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions imports/collections/schemas/registry.js
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,8 @@ export const CorePackageConfig = PackageConfig.clone().extend({
},
"settings.openexchangerates.appId": {
type: String,
label: "Open Exchange Rates App Id"
label: "Open Exchange Rates App Id",
optional: true
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why you needed to make this change since it doesn't appear related to this PR?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@zenweasel Without making this field optional I'm unable to save the shop options form, unless I add an App ID for Open Exchange Rates. Shop options is where I added the sitemap refresh setting & regenerate button. I just noticed that on a fresh install, I also had to make settings.cart in the same schema optional. Does it make sense to have the sitemap settings be in a separate form? Or perhaps somewhere else, like the Shop -> General form?

},
"settings.openexchangerates.refreshPeriod": {
type: String,
Expand Down Expand Up @@ -313,12 +314,23 @@ export const CorePackageConfig = PackageConfig.clone().extend({
},
"settings.cart": {
type: Object,
defaultValue: {}
defaultValue: {},
optional: true
},
"settings.cart.cleanupDurationDays": {
type: String,
label: "Cleanup Schedule",
defaultValue: "older than 3 days"
},
"settings.sitemaps": {
type: Object,
defaultValue: {},
optional: true
},
"settings.sitemaps.refreshPeriod": {
type: String,
label: "Sitemap refresh period",
defaultValue: "every 24 hours"
}
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,11 @@
{{> afQuickField name='settings.google.clientId'}}
{{> afQuickField name='settings.google.apiKey'}}
{{> afQuickField name='settings.cart.cleanupDurationDays' placeholder="older than 3 days"}}
{{#if isPackageEnabled "reaction-sitemap-generator"}}
<div>
{{> React component=SitemapSettingsContainer packageData=packageData}}
</div>
{{/if}}
{{> shopSettingsSubmitButton}}
{{/autoForm}}
</div>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import _ from "lodash";
import { Meteor } from "meteor/meteor";
import { Template } from "meteor/templating";
import { AutoForm } from "meteor/aldeed:autoform";
import { Reaction, i18next } from "/client/api";
import { Packages, Shops } from "/lib/collections";
import { Media } from "/imports/plugins/core/files/client";
import SitemapSettingsContainer from "/imports/plugins/included/sitemap-generator/client/containers/sitemap-settings-container";
import ShopBrandMediaManager from "./ShopBrandMediaManager";


Expand Down Expand Up @@ -34,7 +34,7 @@ Template.shopSettings.helpers({
});

let selectedMediaId;
if (shop && _.isArray(shop.brandAssets)) {
if (shop && Array.isArray(shop.brandAssets)) {
selectedMediaId = shop.brandAssets[0].mediaId;
}

Expand Down Expand Up @@ -157,5 +157,13 @@ Template.optionsShopSettings.helpers({
name: "core",
shopId: Reaction.getShopId()
});
},

isPackageEnabled(name) {
return Reaction.isPackageEnabled(name);
},

SitemapSettingsContainer() {
return SitemapSettingsContainer;
}
});
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ class NotificationRoute extends Component {
});
Reaction.showActionView(actionViewData);
} else {
Reaction.Router.go(notify.url);
// Determine if url's basename ends in a file extension (i.e. /sitemap.xml).
// If so, use window.location to break out of the Reaction app, otherwise navigate via Reaction router
const { url } = notify;
const urlSplit = url.split("/");
const doesBasenameHavePeriod = urlSplit[urlSplit.length - 1].includes(".");
if (doesBasenameHavePeriod) {
window.location = url;
} else {
Reaction.Router.go(url);
}
}

const { markOneAsRead } = this.props;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import React, { Component, Fragment } from "react";
import PropTypes from "prop-types";
import Select from "@reactioncommerce/components/Select/v1";
import Button from "@reactioncommerce/components/Button/v1";
import { Translation } from "/imports/plugins/core/ui/client/components";

export default class SitemapSettings extends Component {
static propTypes = {
onGenerateClick: PropTypes.func.isRequired,
onRefreshPeriodChange: PropTypes.func.isRequired,
refreshPeriod: PropTypes.string.isRequired
};

render() {
const { onRefreshPeriodChange, onGenerateClick, refreshPeriod } = this.props;
const refreshOptions = [
{
label: "Every 24 hours",
value: "every 24 hours"
},
{
label: "Every 12 hours",
value: "every 12 hours"
},
{
label: "Every hour",
value: "every 1 hour"
}
];

return (
<Fragment>
<div className="form-group" data-required="true">
<label className="control-label" htmlFor="settings.sitemaps.refreshPeriod">Sitemap refresh period</label>
<Select
options={refreshOptions}
onChange={onRefreshPeriodChange}
value={refreshPeriod}
/>
<input
type="hidden"
name="settings.sitemaps.refreshPeriod"
data-schema-key="settings.sitemaps.refreshPeriod"
value={refreshPeriod}
/>
</div>
<div className="form-group pull-left" style={{ width: "100%", marginBottom: 30 }}>
<Button actionType="secondary" className="pull-right" isShortHeight onClick={onGenerateClick}>
<i className="rui font-icon fa fa-refresh" />&nbsp;
<Translation defaultValue="Refresh sitemap" i18nKey={"shopSettings.refreshSitemapsNow"} />
</Button>
<p className="pull-right" style={{ marginTop: 6, marginRight: 10 }}>
<a href="/sitemap.xml" target="_blank">View sitemap</a>
</p>
</div>
</Fragment>
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React, { Component } from "react";
import PropTypes from "prop-types";
import { Meteor } from "meteor/meteor";
import { i18next } from "/client/api";
import SitemapSettings from "../components/sitemap-settings";

class SitemapSettingsContainer extends Component {
static propTypes = {
packageData: PropTypes.object
};

constructor(props) {
super(props);
const { packageData } = props;
const { settings } = packageData;
const { sitemaps: sitemapSettings = {} } = settings;

this.state = {
refreshPeriod: sitemapSettings.refreshPeriod || "every 24 hours"
};
}

handleRefreshPeriodChange = (refreshPeriod) => {
if (refreshPeriod) {
this.setState({ refreshPeriod });
}
};

handleGenerateClick = () => {
Meteor.call("sitemaps/generate", (error) => {
if (error) {
Alerts.toast(`${i18next.t("shopSettings.sitemapRefreshFailed", {
defaultValue: "Sitemap refresh failed"
})}: ${error}`, "error");
} else {
Alerts.toast(i18next.t("shopSettings.sitemapRefreshInitiated", {
defaultValue: "Refreshing the sitemap can take up to 5 minutes. You will be notified when it is completed."
}), "success");
}
});
};

render() {
const { refreshPeriod } = this.state;
return (
<SitemapSettings
onRefreshPeriodChange={this.handleRefreshPeriodChange}
onGenerateClick={this.handleGenerateClick}
refreshPeriod={refreshPeriod}
/>
);
}
}

export default SitemapSettingsContainer;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import SimpleSchema from "simpl-schema";
import { registerSchema } from "@reactioncommerce/schemas";
import { check } from "meteor/check";
import { Tracker } from "meteor/tracker";

/**
* @name SitemapsSchema
* @memberof Schemas
* @summary Schema for Sitemaps collection
* @type {SimpleSchema}
*/
export const SitemapsSchema = new SimpleSchema({
shopId: {
type: String
},
handle: {
type: String
},
xml: {
type: String
},
createdAt: {
type: Date
}
}, { check, tracker: Tracker });

registerSchema("Sitemaps", SitemapsSchema);
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Mongo } from "meteor/mongo";
import { SitemapsSchema } from "./schemas/sitemaps-schema";

/**
* @name Sitemaps
* @memberof Collections
* @summary Collection for auto-generated XML sitemaps
*/
export const Sitemaps = new Mongo.Collection("Sitemaps");

Sitemaps.attachSchema(SitemapsSchema);
8 changes: 8 additions & 0 deletions imports/plugins/included/sitemap-generator/register.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import Reaction from "/imports/plugins/core/core/server/Reaction";

Reaction.registerPackage({
label: "Sitemap Generator",
name: "reaction-sitemap-generator",
icon: "fa fa-vine",
autoEnable: true
});
13 changes: 13 additions & 0 deletions imports/plugins/included/sitemap-generator/server/i18n/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[{
"i18n": "en",
"ns": "reaction-sitemap-generator",
"translation": {
"reaction-sitemap-generator": {
"shopSettings": {
"refreshSitemapsNow": "Refresh sitemap",
"sitemapRefreshInitiated": "Refreshing the sitemap can take up to 5 minutes. You will be notified when it is completed.",
"sitemapRefreshFailed": "Sitemap refresh failed"
}
}
}
}]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { loadTranslations } from "/imports/plugins/core/core/server/startup/i18n";

import en from "./en.json";

loadTranslations([en]);
21 changes: 21 additions & 0 deletions imports/plugins/included/sitemap-generator/server/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Meteor } from "meteor/meteor";
import { WebApp } from "meteor/webapp";
import { Sitemaps } from "../lib/collections/sitemaps";
import generateSitemapsJob from "./jobs/generate-sitemaps-job";
import handleSitemapRoutes from "./middleware/handle-sitemap-routes";
import methods from "./methods";

// Load translations
import "./i18n";

// Create a compound index to support queries by shopId or shopId & handle
Sitemaps.rawCollection().createIndex({ shopId: 1, handle: 1 }, { background: true });

// Setup sitemap generation recurring job
generateSitemapsJob();

// Sitemap front-end routes
WebApp.connectHandlers.use(handleSitemapRoutes);

// Init methods
Meteor.methods(methods);
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Hooks from "@reactioncommerce/hooks";
import Logger from "@reactioncommerce/logger";
import { Jobs } from "/lib/collections";
import Reaction from "/imports/plugins/core/core/server/Reaction";
import { Job } from "/imports/plugins/core/job-collection/lib";
import generateSitemaps from "../lib/generate-sitemaps";

/**
* @name generateSitemapsJob
* @summary Initializes and processes a job that regenerates XML sitemaps
* @returns {undefined}
*/
export default function generateSitemapsJob() {
const jobId = "sitemaps/generate";

// Hook that schedules job
Hooks.Events.add("afterCoreInit", () => {
const settings = Reaction.getShopSettings();
const { sitemaps } = settings;
const refreshPeriod = (sitemaps && sitemaps.refreshPeriod) || "every 24 hours";

Logger.debug(`Adding ${jobId} to JobControl. Refresh ${refreshPeriod}`);

new Job(Jobs, jobId, {})
.retry({
retries: 5,
wait: 60000,
backoff: "exponential"
})
.repeat({
schedule: Jobs.later.parse.text(refreshPeriod)
})
.save({
cancelRepeats: true
});
});

// Function that processes job
const sitemapGenerationJob = Jobs.processJobs(jobId, {
pollInterval: 60 * 60 * 1000, // backup polling, see observer below
workTimeout: 180 * 1000
}, (job, callback) => {
Logger.debug(`Processing ${jobId} job`);

const { notifyUserId = "" } = job.data;
generateSitemaps({ notifyUserId });

const doneMessage = `${jobId} job done`;
Logger.debug(doneMessage);
job.done(doneMessage, { repeatId: true });
callback();
});

// Observer that triggers processing of job when ready
Jobs.find({
type: jobId,
status: "ready"
}).observe({
added() {
return sitemapGenerationJob.trigger();
}
});
}
Loading