Using custom WP-API endpoints with the included Backbone JS client library - wp-api

So I have created a custom endpoint using WP-API plugin. When testing with Postman, the endpoint works as expected.
The problem is that when I try to access the custom enpoint using the built in Backbone JS client, I cannot see my new endpoints in the wp.api.models object.
I have been digging and I think I have to send initialise it with a different schema or something, but have aboslutely no idea how to and can't find any info on how to extend wp.api.models so I can access my custom methods.
Example plugin code:
function my_awesome_func( $data ) {
return 'A STRING';
}
function initrest() {
register_rest_route( 'wp/v2', '/score', array(
'methods' => 'GET',
'callback' => 'my_awesome_func',
));
}
add_action( 'rest_api_init', 'initrest');
Example template code:
window.onload = function() {
console.log(wp.api.models);
};
This ouputs a list of the common models for Post, Comment, Tags etc, but no sign of my 'scores' endpoint.

Try initializing the wp api with your own route.
wp.api.init({'versionString' : 'custom-route/v1/', 'apiRoot': 'http://my-website.com/wp-json/'}).done(function()
{
console.log(wp.api.models);
});
Whereas versionString and apiRoot need to be replaced with your own data.

I'm afraid custom endpoints don't make into the wp.api.models by design of the Backbone client. Have a look to my question and the respective answers. They boil down to: "use a different client".
I use node-wpapi.

Related

Vue undefined property Id when using Laravel Echo

I'm learning Vue2 and currently, I have a simple boilerplate using Laravel, Vue2, Laravel Echo, Sanctum, and the Websockets package, all using the Laravel default Echo setup. Everything works nicely there.
Now I'm trying to decouple things, so I've created the same boilerplate but as a Vue SPA and using Laravel Passport (I have my reasons for decoupling things and for using Passport). Everything works fine there as well, including a timeline I've added, but not the real-time stuff.
I need to pass the user id with the Echo event in a few places, so I created a global user object in main.js, like this (I'm using namespaced modules):
let User = store.state.auth.user
Vue.prototype.$user = User
But every time I try passing the user id to an event, the id is undefined, so Pusher cannot authenticate the channel. Though if I console log the user from within any component, I can see the user object with the id. I can also see the event in the Websockets dashboard. So everything works as normal, except passing the id. If I do this:
Echo.private(`timeline.${this.$user.id}`)
.listen('.PostWasCreated', (e) => {
this.PUSH_POSTS([e])
})
The results is this:
private-timeline.undefined
I've also tried other syntax, such as:
Echo.private('timeline.'+this.$user.id)
.listen('.PostWasCreated', (e) => {
this.PUSH_POSTS([e])
})
I'm having a difficult time trying to determine what I'm missing to be able to pass the id.
Try to use a function to defer, instead of an assignment. It is not defined during the assignment.
Vue.prototype.$getUser = ()=>store.state.auth.user
Echo.private(`timeline.${this.$getUser().id}`)
.listen('.PostWasCreated', (e) => {
this.PUSH_POSTS([e])
})
or access the store directly.
Echo.private(`timeline.${this.$store.state.auth.user.id}`)
.listen('.PostWasCreated', (e) => {
this.PUSH_POSTS([e])
})
Or even better, use a getter in your store. (I will leave it to you to add the getter...)
Echo.private(`timeline.${this.$store.getters.userId}`)
.listen('.PostWasCreated', (e) => {
this.PUSH_POSTS([e])
})

How to render a cms page with default theme AND variables from controllers in OctoberCMS?

I'm wondering how I can render a view, or display a page with my default theme in OctoberCMS, via a route that executes a function in a controller.
If I have the following route:
Route::get('bransje', [
'uses' => 'Ekstremedia\Cityportal\CPController#bransje'
]);
And in my controller CPController ive tried several things, like I used to with Laravel:
public function bransje() {
$stuff = Stuff::with('info');
return View::make('cms::bransje')->with('stuff',$stuff);
}
But I cannot seem to get it to work, and I've tried to search the web, but it's hard to find answers. I have found a workaround, and that is to make a plugin component, then I can include that component and do:
public function onRun()
{
$this->eventen = $this->page['stuff'] = $this->stuff();
}
protected function stuff()
{
return ...
}
Is there any way so I can make pages without using the Cms, and that are wrapped in my default theme? I've tried
return View::make('my-theme-name::page');
and a lot of variants but no luck.
I know I can also do a:
==
public function onRun()
{
}
in the start of my page in the cms, but I'm not sure how to call a function from my plugin controller via there.
You can bypass frontend routing by using routes.php file in your plugin.
Full example in this video turotial.
If this answer can still be useful (Worked for October v434).
I have almost the same scenerio.
What I want to achieve is a type of routing like facebook page and profile.
facebook.com/myprofile is the same url structure as facebook.com/mypage
First I create a page in the CMS for each scenario (say catchpage.htm)
Then a created a catchall route at the buttom of routes.php in my plugin that will also not disturb the internal working of octobercms.
if (!Request::is('combine/*') && !Request::is('backend/*') && !Request::is('backend')) {
// Last fail over for looking up slug from the database
Route::get('{slug}/{slug2?}', function ($slug, $slug2 = null) {
//Pretend this are our routes and we can check them against the database
$routes = ["bola", "sade", "bisi", "ade", "tayo"];
if(in_array($slug, $routes)) {
$cmsController = new Cms\Classes\Controller;
return $cmsController->render("/catchpage", ['slug' => $slug]);
}
// Some fallback to 404
return Response::make(View::make('cms::404'), 404);
});
}
The if Request::is check is a list of all the resource that october uses under the hood, please dont remove the combine as it is the combiner route. Remove it and the style and script will not render. Also the backend is the url to the backend, make sure to supply the backend and the backend/*.
Finally don't forget to return Response::make(View::make('cms::404'), 404); if the resource is useless.
You may put all these in a controller though.
If anyone has a better workaround, please let us know.

laravel api with vue 2 js not returning data - could 'localhost:8000' (or '127.0.0.1:8000') be the issue?

I am using the repo https://github.com/mschwarzmueller/laravel-ng2-vue/tree/03-vue-frontend so I have 100% confidence in the reliability of the code. I can post through the laravel api endpoint through the very simple Vue client, and also through Postman. Through Postman I can retrieve the table data array, but not so in the client app. In POSTMAN:
localhost:8000/api/quotes
works just fine.
IN THE vue 2 js CLIENT APP:
methods: {
onGetQuotes() {
axios.get('http://localhost:8000/api/quotes')
.then(
response => {
this.quotes = (response.data.quotes);
}
)
.catch(
error => console.log(error)
);
}
returns nothing. returning the response to Console.log returns nothing. The Network/XHR tab shows the table data rows, but I am not sure what that means.
I know for sure that this code works for others with their unique api endpoints, which I assume may not use localhost or '127:0.0.1:1080.
Edit: in response to request for more info
public function getQuotes()
{
$quotes = Quote::all();
$response = [$quotes];
return response()->json($response, 200);
}
and the relevant route:
Route::get('/quotes', [
'uses' => 'QuoteController#getQuotes'
]);
Just to confirm: I am using verified github repo code in which the ONLY change is my api endpoint addressas mentioned in the first line of the body of this question. . Note that the Laravel back end is also derived from a related repo in Max's fine tutorial. The running code can be seen at
So I really don't think this is a coding error- but is it a configuration error due to me using local host??
EDIT: It WAS a coding error in the laravel controller as shown below
The reason your code isn't working if because you haven't provided a key for your $quotes in your controller but you're looking for it in your vue file (response.data.quotes).
[$quotes] is essentially [0 => $quotes] so when the json response comes through it be 0: [...] not quotes: [...].
To get this to work you just need to change:
$response = [$quotes];
to:
$response = ['quotes' => $quotes];
Furthermore, just an FYI, you don't need to provide the 200 in response->json() as it's the default and you can just return an array and Laravel automatically return the correct json response e.g.:
public function getQuotes()
{
$quotes = \App\Models\Artist::all();
return compact('quotes'); //<-- This is just another way of writting ['quotes' => $quotes]
}
Obviously, you don't have to if you don't want to.
Hope this helps!

How to make a SPA SEO crawlable?

I've been working on how to make a SPA crawlable by google based on google's instructions. Even though there are quite a few general explanations I couldn't find anywhere a more thorough step-by-step tutorial with actual examples. After having finished this I would like to share my solution so that others may also make use of it and possibly improve it further.
I am using MVC with Webapi controllers, and Phantomjs on the server side, and Durandal on the client side with push-state enabled; I also use Breezejs for client-server data interaction, all of which I strongly recommend, but I'll try to give a general enough explanation that will also help people using other platforms.
Before starting, please make sure you understand what google requires, particularly the use of pretty and ugly URLs. Now lets see the implementation:
Client Side
On the client side you only have a single html page which interacts with the server dynamically via AJAX calls. that's what SPA is about. All the a tags in the client side are created dynamically in my application, we'll later see how to make these links visible to google's bot in the server. Each such a tag needs to be able to have a pretty URL in the href tag so that google's bot will crawl it. You don't want the href part to be used when the client clicks on it (even though you do want the server to be able to parse it, we'll see that later), because we may not want a new page to load, only to make an AJAX call getting some data to be displayed in part of the page and change the URL via javascript (e.g. using HTML5 pushstate or with Durandaljs). So, we have both an href attribute for google as well as on onclick which does the job when the user clicks on the link. Now, since I use push-state I don't want any # on the URL, so a typical a tag may look like this:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'category' and 'subCategory' would probably be other phrases, such as 'communication' and 'phones' or 'computers' and 'laptops' for an electrical appliances store. Obviously there would be many different categories and sub categories. As you can see, the link is directly to the category, sub category and the product, not as extra-parameters to a specific 'store' page such as http://www.xyz.com/store/category/subCategory/product111. This is because I prefer shorter and simpler links. It implies that I there will not be a category with the same name as one of my 'pages', i.e. 'about'.
I will not go into how to load the data via AJAX (the onclick part), search it on google, there are many good explanations. The only important thing here that I do want to mention is that when the user clicks on this link, I want the URL in the browser to look like this:
http://www.xyz.com/category/subCategory/product111. And this is URL is not sent to the server ! remember, this is a SPA where all the interaction between the client and the server is done via AJAX, no links at all! all 'pages' are implemented on the client side, and the different URL does not make a call to the server (the server does need to know how to handle these URLs in case they are used as external links from another site to your site, we'll see that later on the server side part). Now, this is handled wonderfully by Durandal. I strongly recommend it, but you can also skip this part if you prefer other technologies. If you do choose it, and you're also using MS Visual Studio Express 2012 for Web like me, you can install the Durandal Starter Kit, and there, in shell.js, use something like this:
define(['plugins/router', 'durandal/app'], function (router, app) {
return {
router: router,
activate: function () {
router.map([
{ route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
{ route: 'about', moduleId: 'viewmodels/about', nav: true }
])
.buildNavigationModel()
.mapUnknownRoutes(function (instruction) {
instruction.config.moduleId = 'viewmodels/store';
instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of push-state, only ! remains
return instruction;
});
return router.activate({ pushState: true });
}
};
});
There are a few important things to notice here:
The first route (with route:'') is for the URL which has no extra data in it, i.e. http://www.xyz.com. In this page you load general data using AJAX. There may actually be no a tags at all in this page. You will want to add the following tag so that google's bot will know what to do with it:
<meta name="fragment" content="!">. This tag will make google's bot transform the URL to www.xyz.com?_escaped_fragment_= which we'll see later.
The 'about' route is just an example to a link to other 'pages' you may want on your web application.
Now, the tricky part is that there is no 'category' route, and there may be many different categories - none of which have a predefined route. This is where mapUnknownRoutes comes in. It maps these unknown routes to the 'store' route and also removes any '!' from the URL in case it's a pretty URL generated by google's seach engine. The 'store' route takes the info in the 'fragment' property and makes the AJAX call to get the data, display it, and change the URL locally. In my application, I don't load a different page for every such call; I only change the part of the page where this data is relevant and also change the URL locally.
Notice the pushState:true which instructs Durandal to use push state URLs.
This is all we need in the client side. It can be implemented also with hashed URLs (in Durandal you simple remove the pushState:true for that). The more complex part (at least for me...) was the server part:
Server Side
I'm using MVC 4.5 on the server side with WebAPI controllers. The server actually needs to handle 3 types of URLs: the ones generated by google - both pretty and ugly and also a 'simple' URL with the same format as the one that appears in the client's browser. Lets look on how to do this:
Pretty URLs and 'simple' ones are first interpreted by the server as if trying to reference a non-existent controller. The server sees something like http://www.xyz.com/category/subCategory/product111 and looks for a controller named 'category'. So in web.config I add the following line to redirect these to a specific error handling controller:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Now, this transforms the URL to something like: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111. I want the URL to be sent to the client that will load the data via AJAX, so the trick here is to call the default 'index' controller as if not referencing any controller; I do that by adding a hash to the URL before all the 'category' and 'subCategory' parameters; the hashed URL does not require any special controller except the default 'index' controller and the data is sent to the client which then removes the hash and uses the info after the hash to load the data via AJAX. Here is the error handler controller code:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;
using System.Web.Routing;
namespace eShop.Controllers
{
public class ErrorController : ApiController
{
[HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
public HttpResponseMessage Handle404()
{
string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
var response = Request.CreateResponse(HttpStatusCode.Redirect);
response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
return response;
}
}
}
But what about the Ugly URLs? These are created by google's bot and should return plain HTML that contains all the data the user sees in the browser. For this I use phantomjs. Phantom is a headless browser doing what the browser is doing on the client side - but on the server side. In other words, phantom knows (among other things) how to get a web page via a URL, parse it including running all the javascript code in it (as well as getting data via AJAX calls), and give you back the HTML that reflects the DOM. If you're using MS Visual Studio Express you many want to install phantom via this link.
But first, when an ugly URL is sent to the server, we must catch it; For this, I added to the 'App_start' folder the following file:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;
namespace eShop.App_Start
{
public class AjaxCrawlableAttribute : ActionFilterAttribute
{
private const string Fragment = "_escaped_fragment_";
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
var request = filterContext.RequestContext.HttpContext.Request;
if (request.QueryString[Fragment] != null)
{
var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");
filterContext.Result = new RedirectToRouteResult(
new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
}
return;
}
}
}
This is called from 'filterConfig.cs' also in 'App_start':
using System.Web.Mvc;
using eShop.App_Start;
namespace eShop
{
public class FilterConfig
{
public static void RegisterGlobalFilters(GlobalFilterCollection filters)
{
filters.Add(new HandleErrorAttribute());
filters.Add(new AjaxCrawlableAttribute());
}
}
}
As you can see, 'AjaxCrawlableAttribute' routes ugly URLs to a controller named 'HtmlSnapshot', and here is this controller:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;
namespace eShop.Controllers
{
public class HtmlSnapshotController : Controller
{
public ActionResult returnHTML(string url)
{
string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
var startInfo = new ProcessStartInfo
{
Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
UseShellExecute = false,
CreateNoWindow = true,
RedirectStandardOutput = true,
RedirectStandardError = true,
RedirectStandardInput = true,
StandardOutputEncoding = System.Text.Encoding.UTF8
};
var p = new Process();
p.StartInfo = startInfo;
p.Start();
string output = p.StandardOutput.ReadToEnd();
p.WaitForExit();
ViewData["result"] = output;
return View();
}
}
}
The associated view is very simple, just one line of code:
#Html.Raw( ViewBag.result )
As you can see in the controller, phantom loads a javascript file named createSnapshot.js under a folder I created called seo. Here is this javascript file:
var page = require('webpage').create();
var system = require('system');
var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();
page.onResourceReceived = function (response) {
if (requestIds.indexOf(response.id) !== -1) {
lastReceived = new Date().getTime();
responseCount++;
requestIds[requestIds.indexOf(response.id)] = null;
}
};
page.onResourceRequested = function (request) {
if (requestIds.indexOf(request.id) === -1) {
requestIds.push(request.id);
requestCount++;
}
};
function checkLoaded() {
return page.evaluate(function () {
return document.all["compositionComplete"];
}) != null;
}
// Open the page
page.open(system.args[1], function () { });
var checkComplete = function () {
// We don't allow it to take longer than 5 seconds but
// don't return until all requests are finished
if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
clearInterval(checkCompleteInterval);
var result = page.content;
//result = result.substring(0, 10000);
console.log(result);
//console.log(results);
phantom.exit();
}
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);
I first want to thank Thomas Davis for the page where I got the basic code from :-).
You will notice something odd here: phantom keeps re-loading the page until the checkLoaded() function returns true. Why is that? this is because my specific SPA makes several AJAX call to get all the data and place it in the DOM on my page, and phantom cannot know when all the calls have completed before returning me back the HTML reflection of the DOM. What I did here is after the final AJAX call I add a <span id='compositionComplete'></span>, so that if this tag exists I know the DOM is completed. I do this in response to Durandal's compositionComplete event, see here for more. If this does not happen withing 10 seconds I give up (it should take only a second to so the most). The HTML returned contains all the links that the user sees in the browser. The script will not work properly because the <script> tags that do exist in the HTML snapshot do not reference the right URL. This can be changed too in the javascript phantom file, but I don't think this is necassary because the HTML snapshort is only used by google to get the a links and not to run javascript; these links do reference a pretty URL, and if fact, if you try to see the HTML snapshot in a browser, you will get javascript errors but all the links will work properly and direct you to the server once again with a pretty URL this time getting the fully working page.
This is it. Now the server know how to handle both pretty and ugly URLs, with push-state enabled on both server and client. All ugly URLs are treated the same way using phantom so there's no need to create a separate controller for each type of call.
One thing you might prefer to change is not to make a general 'category/subCategory/product' call but to add a 'store' so that the link will look something like: http://www.xyz.com/store/category/subCategory/product111. This will avoid the problem in my solution that all invalid URLs are treated as if they are actually calls to the 'index' controller, and I suppose that these can be handled then within the 'store' controller without the addition to the web.config I showed above.
Google is now able to render SPA pages:
Deprecating our AJAX crawling scheme
Here is a link to a screencast-recording from my Ember.js Training class I hosted in London on August 14th. It outlines a strategy for both your client-side application and for you server-side application, as well as gives a live demonstration of how implementing these features will provide your JavaScript Single-Page-App with graceful degradation even for users with JavaScript turned off.
It uses PhantomJS to aid in crawling your website.
In short, the steps required are:
Have a hosted version of the web application you want to crawl, this site needs to have ALL of the data you have in production
Write a JavaScript application (PhantomJS Script) to load your website
Add index.html ( or “/“ ) to the list of URLs to crawl
Pop the first URL added to the crawl-list
Load page and render its DOM
Find any links on the loaded page that links to your own site (URL filtering)
Add this link to a list of “crawlable” URLS, if its not already crawled
Store the rendered DOM to a file on the file system, but strip away ALL script-tags first
At the end, create a Sitemap.xml file with the crawled URLs
Once this step is done, its up to your backend to serve the static-version of your HTML as part of the noscript-tag on that page. This will allow Google and other search engines to crawl every single page on your website, even though your app originally is a single-page-app.
Link to the screencast with the full details:
http://www.devcasts.io/p/spas-phantomjs-and-seo/#
I used Rendertron to solve the SEO problem in ASP.net core and Angular on the client side, it is a middleware that differentiates requests based on being crawler or client, so when the request is from the crawler side the response generated with briefly and quickly on the fly.
rendered site for normal clients:
rendered site for Crawlers:
In Startup.cs
Configure rendertron services:
public void ConfigureServices(IServiceCollection services)
{
// Add rendertron services
services.AddRendertron(options =>
{
// rendertron service url
options.RendertronUrl = "http://rendertron:3000/render/";
// proxy url for application
options.AppProxyUrl = "http://webapplication";
// prerender for firefox
//options.UserAgents.Add("firefox");
// inject shady dom
options.InjectShadyDom = true;
// use http compression
options.AcceptCompression = true;
});
}
It is true that this method is a little different and requires a short code to produce content specific to the crawler, but it is useful for small projects such as CMS or portal site, etc.
This method can be done in most programming languages ​​or server-side frameworks such as ASP.net core, Python (Django), Express.js, Firebase.
To view the source and more details: https://github.com/GoogleChrome/rendertron
Year 2021 Update
SPA should use History API in order to be SEO friendly.
Transitions between SPA pages are typically effected via history.pushState(path) call. What happens next is framework dependent. In case React is used, a component called React Router monitors history and displays/renders the React component configured for the path used.
Achieving SEO for a simple SPA is straightforward.
Achieving SEO for a more advanced SPA (that uses selective prerendering for better performance) is more involved as shown in the article. I'm the author.
You can use or create your own service for prerender your SPA with the service called prerender. You can check it out on his website prerender.io and on his github project (It uses PhantomJS and it renderize your website for you).
It's very easy to start with. You only have to redirect crawlers requests to the service and they will receive the rendered html.

Backbone JS and CodeIgniter REST Server

I have a standard CI web app, but I've decided to get the chaotic javascript in order using backbone. I had a whole pile of serialized forms/jQuery AJAX requests to various controller methods: authenticate, change_password, register_member, request_new_password, etc.., and don't quite understand how REST works instead. I'm using Phil Sturgeon's REST library for CI https://github.com/philsturgeon/codeigniter-restserver
Should every backbone model have a different api url? And what am I supposed to actually call the controller methods?
<?php
require(APPPATH.'/libraries/REST_Controller.php');
class RestApi extends REST_Controller
{
function get()
{
But it just 404s.
I just don't get how to replace the routing to fifty of my old methods based on a handful of HTTP methods. Does the name of the backbone model need to match something on the server side?
You have to name your functions index_HTTPMETHOD. In your example it would be:
class RestApi extends REST_Controller {
// this will handle GET http://.../RestApi
function index_get() {
}
// additionally this will handle POST http://.../RestApi
function index_post() {
}
// and so forth
// if you want to POST to http://.../RestApi/somefunc
function somefunc_post() {
}
}
the url-attribute of the model should match the server-side 'url' which returns the JSON that will make up the model's attributes. Backbone.js has default functionality to this, which is to match the model's collection url with it's id attribute. The collection url requirement can be foregone by overriding the urlRoot -function, in order to operate model's outside of collections.
If you want to be independent of the id -attribute as well, you sould override the url -attribute/function to provide your own url that matches to the model on the server, like this:
url: 'path/to/my/model'
or
url: function() { // Define the url as a function of some model properties
var path = this.model_root + '/' + 'some_other_url_fragment/' + this.chosen_model_identifier;
return path;
}

Resources